mbeditor 0.5.3 → 0.7.1
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 +77 -0
- data/README.md +7 -0
- data/app/assets/javascripts/mbeditor/application.js +3 -0
- data/app/assets/javascripts/mbeditor/components/ChangelogView.js +145 -0
- data/app/assets/javascripts/mbeditor/components/DiffViewer.js +1 -1
- data/app/assets/javascripts/mbeditor/components/EditorPanel.js +359 -31
- data/app/assets/javascripts/mbeditor/components/FileTree.js +177 -116
- data/app/assets/javascripts/mbeditor/components/MbeditorApp.js +952 -143
- data/app/assets/javascripts/mbeditor/components/TabBar.js +9 -0
- data/app/assets/javascripts/mbeditor/conflict_parser.js +48 -0
- data/app/assets/javascripts/mbeditor/editor_plugins.js +420 -67
- data/app/assets/javascripts/mbeditor/editor_store.js +1 -0
- data/app/assets/javascripts/mbeditor/file_service.js +34 -6
- data/app/assets/javascripts/mbeditor/git_service.js +2 -1
- data/app/assets/javascripts/mbeditor/history_service.js +177 -0
- data/app/assets/javascripts/mbeditor/search_service.js +1 -0
- data/app/assets/javascripts/mbeditor/tab_manager.js +8 -5
- data/app/assets/stylesheets/mbeditor/application.css +112 -0
- data/app/assets/stylesheets/mbeditor/editor.css +443 -78
- data/app/channels/mbeditor/editor_channel.rb +5 -41
- data/app/controllers/mbeditor/application_controller.rb +8 -1
- data/app/controllers/mbeditor/editors_controller.rb +276 -654
- data/app/controllers/mbeditor/git_controller.rb +2 -61
- data/app/services/mbeditor/availability_probe.rb +83 -0
- data/app/services/mbeditor/code_search_service.rb +42 -0
- data/app/services/mbeditor/editor_state_service.rb +91 -0
- data/app/services/mbeditor/exclusion_matcher.rb +23 -0
- data/app/services/mbeditor/file_operation_service.rb +68 -0
- data/app/services/mbeditor/file_tree_service.rb +69 -0
- data/app/services/mbeditor/git_combined_diff_service.rb +43 -0
- data/app/services/mbeditor/git_commit_detail_service.rb +46 -0
- data/app/services/mbeditor/git_info_service.rb +151 -0
- data/app/services/mbeditor/git_service.rb +36 -26
- data/app/services/mbeditor/js_definition_service.rb +59 -0
- data/app/services/mbeditor/js_members_service.rb +62 -0
- data/app/services/mbeditor/process_runner.rb +48 -0
- data/app/services/mbeditor/rails_related_files_service.rb +282 -0
- data/app/services/mbeditor/ruby_definition_service.rb +77 -101
- data/app/services/mbeditor/schema_service.rb +270 -0
- data/app/services/mbeditor/search_replace_service.rb +184 -0
- data/app/services/mbeditor/test_runner_service.rb +5 -27
- data/app/views/layouts/mbeditor/application.html.erb +2 -2
- data/config/routes.rb +8 -1
- data/lib/mbeditor/configuration.rb +4 -2
- data/lib/mbeditor/version.rb +1 -1
- data/public/monaco-editor/vs/language/css/cssMode.js +13 -0
- data/public/monaco-editor/vs/language/css/cssWorker.js +77 -0
- data/public/monaco-editor/vs/language/html/htmlMode.js +13 -0
- data/public/monaco-editor/vs/language/html/htmlWorker.js +454 -0
- data/public/monaco-editor/vs/language/json/jsonMode.js +19 -0
- data/public/monaco-editor/vs/language/json/jsonWorker.js +42 -0
- metadata +26 -3
- data/app/services/mbeditor/unused_methods_service.rb +0 -139
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "digest"
|
|
3
4
|
require "fileutils"
|
|
4
5
|
require "open3"
|
|
5
6
|
require "shellwords"
|
|
@@ -14,8 +15,6 @@ module Mbeditor
|
|
|
14
15
|
before_action :verify_mbeditor_client, unless: -> { request.get? || request.head? }
|
|
15
16
|
|
|
16
17
|
IMAGE_EXTENSIONS = %w[png jpg jpeg gif svg ico webp bmp avif].freeze
|
|
17
|
-
MAX_OPEN_FILE_SIZE_BYTES = 5 * 1024 * 1024
|
|
18
|
-
RG_AVAILABLE = system("which rg > /dev/null 2>&1")
|
|
19
18
|
|
|
20
19
|
# GET /mbeditor — renders the IDE shell
|
|
21
20
|
def index
|
|
@@ -33,11 +32,11 @@ module Mbeditor
|
|
|
33
32
|
render json: {
|
|
34
33
|
rootName: workspace_root.basename.to_s,
|
|
35
34
|
rootPath: workspace_root.to_s,
|
|
36
|
-
rubocopAvailable:
|
|
35
|
+
rubocopAvailable: AvailabilityProbe.rubocop(workspace_root),
|
|
37
36
|
rubocopConfigPath: rubocop_config_path,
|
|
38
|
-
hamlLintAvailable:
|
|
39
|
-
gitAvailable:
|
|
40
|
-
blameAvailable:
|
|
37
|
+
hamlLintAvailable: AvailabilityProbe.haml_lint(workspace_root),
|
|
38
|
+
gitAvailable: AvailabilityProbe.git(workspace_root),
|
|
39
|
+
blameAvailable: AvailabilityProbe.git(workspace_root),
|
|
41
40
|
redmineEnabled: Mbeditor.configuration.redmine_enabled == true,
|
|
42
41
|
testAvailable: test_available?,
|
|
43
42
|
actionCableEnabled: action_cable_enabled?
|
|
@@ -46,67 +45,40 @@ module Mbeditor
|
|
|
46
45
|
|
|
47
46
|
# GET /mbeditor/files — recursive file tree
|
|
48
47
|
def files
|
|
49
|
-
|
|
50
|
-
cached = cached_file_tree(root)
|
|
51
|
-
return render json: cached if cached
|
|
52
|
-
|
|
53
|
-
tree = build_tree(root)
|
|
54
|
-
store_file_tree(root, tree)
|
|
55
|
-
render json: tree
|
|
48
|
+
render json: FileTreeService.build(workspace_root)
|
|
56
49
|
end
|
|
57
50
|
|
|
58
51
|
# GET /mbeditor/state — load workspace state
|
|
59
52
|
def state
|
|
60
|
-
|
|
61
|
-
if File.exist?(path)
|
|
62
|
-
render json: JSON.parse(File.read(path))
|
|
63
|
-
else
|
|
64
|
-
render json: {}
|
|
65
|
-
end
|
|
66
|
-
rescue Errno::ENOENT
|
|
67
|
-
render json: {}
|
|
68
|
-
rescue JSON::ParserError
|
|
69
|
-
render json: {}
|
|
53
|
+
render json: editor_state_service.read_state
|
|
70
54
|
rescue StandardError => e
|
|
71
55
|
render json: { error: e.message }, status: :unprocessable_content
|
|
72
56
|
end
|
|
73
57
|
|
|
74
|
-
STATE_MAX_BYTES =
|
|
58
|
+
STATE_MAX_BYTES = EditorStateService::STATE_MAX_BYTES
|
|
75
59
|
|
|
76
60
|
# POST /mbeditor/state — save workspace state
|
|
77
61
|
def save_state
|
|
78
62
|
raw = params[:state]
|
|
79
63
|
raw = raw.to_unsafe_h if raw.respond_to?(:to_unsafe_h)
|
|
80
|
-
|
|
81
|
-
return render json: { error: "State payload too large" }, status: :content_too_large if payload.bytesize > STATE_MAX_BYTES
|
|
82
|
-
|
|
83
|
-
path = workspace_root.join("tmp", "mbeditor_workspace.json")
|
|
84
|
-
FileUtils.mkdir_p(workspace_root.join("tmp"))
|
|
85
|
-
File.open(path, File::RDWR | File::CREAT) do |f|
|
|
86
|
-
f.flock(File::LOCK_EX)
|
|
87
|
-
f.truncate(0)
|
|
88
|
-
f.rewind
|
|
89
|
-
f.write(payload)
|
|
90
|
-
end
|
|
64
|
+
editor_state_service.write_state(raw)
|
|
91
65
|
render json: { ok: true }
|
|
66
|
+
rescue EditorStateService::PayloadTooLargeError
|
|
67
|
+
render json: { error: "State payload too large" }, status: :content_too_large
|
|
92
68
|
rescue StandardError => e
|
|
93
69
|
render json: { error: e.message }, status: :unprocessable_content
|
|
94
70
|
end
|
|
95
71
|
|
|
96
|
-
|
|
72
|
+
HISTORY_MAX_OPS = 10_000
|
|
73
|
+
HISTORY_COMPACT_TARGET = 5_000
|
|
74
|
+
|
|
97
75
|
|
|
98
76
|
# GET /mbeditor/branch_state?branch=... — load per-branch pane state
|
|
99
77
|
def branch_state
|
|
100
78
|
branch = sanitize_branch_name(params[:branch])
|
|
101
79
|
return render json: {}, status: :bad_request unless branch
|
|
102
80
|
|
|
103
|
-
|
|
104
|
-
if File.exist?(path)
|
|
105
|
-
all = JSON.parse(File.read(path))
|
|
106
|
-
render json: (all[branch] || {})
|
|
107
|
-
else
|
|
108
|
-
render json: {}
|
|
109
|
-
end
|
|
81
|
+
render json: editor_state_service.read_branch_state(branch)
|
|
110
82
|
rescue StandardError
|
|
111
83
|
render json: {}
|
|
112
84
|
end
|
|
@@ -115,50 +87,114 @@ module Mbeditor
|
|
|
115
87
|
return render json: { error: "Invalid branch name" }, status: :bad_request unless branch
|
|
116
88
|
|
|
117
89
|
payload = params[:state].to_unsafe_h
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
path = workspace_root.join("tmp", "mbeditor_branch_states.json")
|
|
121
|
-
FileUtils.mkdir_p(workspace_root.join("tmp"))
|
|
122
|
-
File.open(path, File::RDWR | File::CREAT) do |f|
|
|
123
|
-
f.flock(File::LOCK_EX)
|
|
124
|
-
existing = f.size > 0 ? JSON.parse(f.read) : {}
|
|
125
|
-
existing[branch] = payload
|
|
126
|
-
f.truncate(0)
|
|
127
|
-
f.rewind
|
|
128
|
-
f.write(existing.to_json)
|
|
129
|
-
end
|
|
90
|
+
editor_state_service.write_branch_state(branch, payload)
|
|
130
91
|
render json: { ok: true }
|
|
92
|
+
rescue EditorStateService::PayloadTooLargeError
|
|
93
|
+
render json: { error: "State payload too large" }, status: :content_too_large
|
|
131
94
|
rescue StandardError => e
|
|
132
95
|
render json: { error: e.message }, status: :unprocessable_content
|
|
133
96
|
end
|
|
134
97
|
|
|
135
98
|
# POST /mbeditor/prune_branch_states — remove states for deleted branches
|
|
136
99
|
def prune_branch_states
|
|
137
|
-
state_path = workspace_root.join("tmp", "mbeditor_branch_states.json")
|
|
138
|
-
return render json: { pruned: [] } unless File.exist?(state_path)
|
|
139
|
-
|
|
140
100
|
root = workspace_root.to_s
|
|
141
101
|
out, _err, status = Open3.capture3("git", "-C", root, "branch", "--format=%(refname:short)")
|
|
142
102
|
return render json: { pruned: [] } unless status.success?
|
|
143
103
|
|
|
144
104
|
local_branches = out.split("\n").map(&:strip).reject(&:empty?)
|
|
145
|
-
pruned =
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
f.rewind
|
|
154
|
-
f.write(all.to_json)
|
|
105
|
+
pruned = editor_state_service.prune_branch_states(active_branches: local_branches)
|
|
106
|
+
|
|
107
|
+
hist_dir = workspace_root.join('tmp', 'mbeditor_history')
|
|
108
|
+
if File.directory?(hist_dir)
|
|
109
|
+
Dir.glob(File.join(hist_dir, '*.json')) do |hist_file|
|
|
110
|
+
data = JSON.parse(File.read(hist_file)) rescue nil
|
|
111
|
+
next unless data.is_a?(Hash) && data['branch']
|
|
112
|
+
FileUtils.rm_f(hist_file) unless local_branches.include?(data['branch'])
|
|
155
113
|
end
|
|
156
114
|
end
|
|
115
|
+
|
|
157
116
|
render json: { pruned: pruned }
|
|
158
117
|
rescue StandardError => e
|
|
159
118
|
render json: { error: e.message }, status: :unprocessable_content
|
|
160
119
|
end
|
|
161
120
|
|
|
121
|
+
# GET /mbeditor/file_history?branch=X&path=Y
|
|
122
|
+
def file_history
|
|
123
|
+
branch = sanitize_branch_name(params[:branch])
|
|
124
|
+
return render json: {}, status: :bad_request unless branch
|
|
125
|
+
|
|
126
|
+
path = resolve_path(params[:path])
|
|
127
|
+
return render json: {}, status: :forbidden unless path
|
|
128
|
+
|
|
129
|
+
rel = relative_path(path)
|
|
130
|
+
hist = history_file_path(branch, rel)
|
|
131
|
+
return render json: {} unless File.exist?(hist)
|
|
132
|
+
|
|
133
|
+
data = JSON.parse(File.read(hist))
|
|
134
|
+
|
|
135
|
+
if data['t']
|
|
136
|
+
age = Time.now.utc - Time.parse(data['t'])
|
|
137
|
+
if age > 7 * 24 * 3600
|
|
138
|
+
FileUtils.rm_f(hist)
|
|
139
|
+
return render json: {}
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
render json: { base: data['base'], ops: data['ops'] || [] }
|
|
144
|
+
rescue JSON::ParserError
|
|
145
|
+
FileUtils.rm_f(hist) rescue nil
|
|
146
|
+
render json: {}
|
|
147
|
+
rescue StandardError
|
|
148
|
+
render json: {}
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# POST /mbeditor/file_history
|
|
152
|
+
def save_file_history
|
|
153
|
+
branch = sanitize_branch_name(params[:branch])
|
|
154
|
+
return render json: { error: 'Invalid branch name' }, status: :bad_request unless branch
|
|
155
|
+
|
|
156
|
+
path = resolve_path(params[:path])
|
|
157
|
+
return render json: { error: 'Forbidden' }, status: :forbidden unless path
|
|
158
|
+
|
|
159
|
+
rel = relative_path(path)
|
|
160
|
+
new_ops = params[:ops]
|
|
161
|
+
return render json: { error: 'ops must be an array' }, status: :bad_request unless new_ops.is_a?(Array)
|
|
162
|
+
return head :no_content if new_ops.empty?
|
|
163
|
+
|
|
164
|
+
new_ops_clean = new_ops.map { |op| Array(op).first(5) }
|
|
165
|
+
|
|
166
|
+
hist = history_file_path(branch, rel)
|
|
167
|
+
FileUtils.mkdir_p(File.dirname(hist))
|
|
168
|
+
|
|
169
|
+
File.open(hist, File::RDWR | File::CREAT) do |f|
|
|
170
|
+
f.flock(File::LOCK_EX)
|
|
171
|
+
existing = f.size > 0 ? (JSON.parse(f.read) rescue {}) : {}
|
|
172
|
+
|
|
173
|
+
if existing.empty?
|
|
174
|
+
base = params[:base].to_s
|
|
175
|
+
return render json: { error: 'base required for initial history' }, status: :bad_request if base.empty?
|
|
176
|
+
return render json: { error: 'base too large' }, status: :content_too_large if base.bytesize > STATE_MAX_BYTES
|
|
177
|
+
existing = { 'branch' => branch, 'path' => rel, 'base' => base, 'ops' => [], 't' => Time.now.utc.iso8601 }
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
existing['ops'] = (existing['ops'] || []) + new_ops_clean
|
|
181
|
+
existing['t'] = Time.now.utc.iso8601
|
|
182
|
+
|
|
183
|
+
if existing['ops'].length > HISTORY_MAX_OPS
|
|
184
|
+
to_compact = existing['ops'].shift(HISTORY_COMPACT_TARGET)
|
|
185
|
+
existing['base'] = compact_history_ops(existing['base'], to_compact)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
f.truncate(0)
|
|
189
|
+
f.rewind
|
|
190
|
+
f.write(existing.to_json)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
head :no_content
|
|
194
|
+
rescue StandardError => e
|
|
195
|
+
render json: { error: e.message }, status: :unprocessable_content
|
|
196
|
+
end
|
|
197
|
+
|
|
162
198
|
# GET /mbeditor/file?path=...
|
|
163
199
|
def show
|
|
164
200
|
path = resolve_path(params[:path])
|
|
@@ -194,7 +230,7 @@ module Mbeditor
|
|
|
194
230
|
end
|
|
195
231
|
|
|
196
232
|
size = File.size(path)
|
|
197
|
-
return render_file_too_large(size) if size >
|
|
233
|
+
return render_file_too_large(size) if size > FileOperationService::MAX_FILE_SIZE_BYTES
|
|
198
234
|
|
|
199
235
|
if image_path?(path)
|
|
200
236
|
return render json: {
|
|
@@ -223,7 +259,7 @@ module Mbeditor
|
|
|
223
259
|
return render json: { error: "Not found" }, status: :not_found unless File.file?(path)
|
|
224
260
|
|
|
225
261
|
size = File.size(path)
|
|
226
|
-
return render_file_too_large(size) if size >
|
|
262
|
+
return render_file_too_large(size) if size > FileOperationService::MAX_FILE_SIZE_BYTES
|
|
227
263
|
|
|
228
264
|
send_file path, disposition: "inline"
|
|
229
265
|
end
|
|
@@ -234,12 +270,11 @@ module Mbeditor
|
|
|
234
270
|
return render json: { error: "Forbidden" }, status: :forbidden unless path
|
|
235
271
|
return render json: { error: "Cannot write to this path" }, status: :forbidden if path_blocked_for_operations?(path)
|
|
236
272
|
|
|
237
|
-
|
|
238
|
-
return render_file_too_large(content.bytesize) if content.bytesize > MAX_OPEN_FILE_SIZE_BYTES
|
|
239
|
-
|
|
240
|
-
File.write(path, content)
|
|
273
|
+
result = FileOperationService.new(workspace_root).save(path, params[:code].to_s)
|
|
241
274
|
broadcast_files_changed
|
|
242
|
-
render json:
|
|
275
|
+
render json: result
|
|
276
|
+
rescue FileOperationService::FileTooLargeError
|
|
277
|
+
render_file_too_large(params[:code].to_s.bytesize)
|
|
243
278
|
rescue StandardError => e
|
|
244
279
|
render json: { error: e.message }, status: :unprocessable_content
|
|
245
280
|
end
|
|
@@ -249,16 +284,14 @@ module Mbeditor
|
|
|
249
284
|
path = resolve_path(params[:path])
|
|
250
285
|
return render json: { error: "Forbidden" }, status: :forbidden unless path
|
|
251
286
|
return render json: { error: "Cannot create file in this path" }, status: :forbidden if path_blocked_for_operations?(path)
|
|
252
|
-
return render json: { error: "File already exists" }, status: :unprocessable_content if File.exist?(path)
|
|
253
|
-
|
|
254
|
-
content = params[:code].to_s
|
|
255
|
-
return render_file_too_large(content.bytesize) if content.bytesize > MAX_OPEN_FILE_SIZE_BYTES
|
|
256
287
|
|
|
257
|
-
|
|
258
|
-
File.write(path, content)
|
|
288
|
+
result = FileOperationService.new(workspace_root).create_file(path, params[:code].to_s)
|
|
259
289
|
broadcast_files_changed
|
|
260
|
-
|
|
261
|
-
|
|
290
|
+
render json: result
|
|
291
|
+
rescue FileOperationService::FileExistsError
|
|
292
|
+
render json: { error: "File already exists" }, status: :unprocessable_content
|
|
293
|
+
rescue FileOperationService::FileTooLargeError
|
|
294
|
+
render_file_too_large(params[:code].to_s.bytesize)
|
|
262
295
|
rescue StandardError => e
|
|
263
296
|
render json: { error: e.message }, status: :unprocessable_content
|
|
264
297
|
end
|
|
@@ -268,11 +301,12 @@ module Mbeditor
|
|
|
268
301
|
path = resolve_path(params[:path])
|
|
269
302
|
return render json: { error: "Forbidden" }, status: :forbidden unless path
|
|
270
303
|
return render json: { error: "Cannot create directory in this path" }, status: :forbidden if path_blocked_for_operations?(path)
|
|
271
|
-
return render json: { error: "Path already exists" }, status: :unprocessable_content if File.exist?(path)
|
|
272
304
|
|
|
273
|
-
|
|
305
|
+
result = FileOperationService.new(workspace_root).create_dir(path)
|
|
274
306
|
broadcast_files_changed
|
|
275
|
-
render json:
|
|
307
|
+
render json: result
|
|
308
|
+
rescue FileOperationService::FileExistsError
|
|
309
|
+
render json: { error: "Path already exists" }, status: :unprocessable_content
|
|
276
310
|
rescue StandardError => e
|
|
277
311
|
render json: { error: e.message }, status: :unprocessable_content
|
|
278
312
|
end
|
|
@@ -282,20 +316,15 @@ module Mbeditor
|
|
|
282
316
|
old_path = resolve_path(params[:path])
|
|
283
317
|
new_path = resolve_path(params[:new_path])
|
|
284
318
|
return render json: { error: "Forbidden" }, status: :forbidden unless old_path && new_path
|
|
285
|
-
return render json: { error: "Path not found" }, status: :not_found unless File.exist?(old_path)
|
|
286
|
-
return render json: { error: "Target path already exists" }, status: :unprocessable_content if File.exist?(new_path)
|
|
287
319
|
return render json: { error: "Cannot rename this path" }, status: :forbidden if path_blocked_for_operations?(old_path) || path_blocked_for_operations?(new_path)
|
|
288
320
|
|
|
289
|
-
|
|
290
|
-
FileUtils.mv(old_path, new_path)
|
|
321
|
+
result = FileOperationService.new(workspace_root).rename(old_path, new_path)
|
|
291
322
|
broadcast_files_changed
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
name: File.basename(new_path)
|
|
298
|
-
}
|
|
323
|
+
render json: result
|
|
324
|
+
rescue FileOperationService::PathNotFoundError
|
|
325
|
+
render json: { error: "Path not found" }, status: :not_found
|
|
326
|
+
rescue FileOperationService::TargetExistsError
|
|
327
|
+
render json: { error: "Target path already exists" }, status: :unprocessable_content
|
|
299
328
|
rescue StandardError => e
|
|
300
329
|
render json: { error: e.message }, status: :unprocessable_content
|
|
301
330
|
end
|
|
@@ -304,19 +333,11 @@ module Mbeditor
|
|
|
304
333
|
def destroy_path
|
|
305
334
|
path = resolve_path(params[:path])
|
|
306
335
|
return render json: { error: "Forbidden" }, status: :forbidden unless path
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
FileUtils.rm_rf(path)
|
|
313
|
-
broadcast_files_changed
|
|
314
|
-
render json: { ok: true, type: "folder", path: relative_path(path) }
|
|
315
|
-
else
|
|
316
|
-
File.delete(path)
|
|
317
|
-
broadcast_files_changed
|
|
318
|
-
render json: { ok: true, type: "file", path: relative_path(path) }
|
|
319
|
-
end
|
|
336
|
+
return render json: { error: "Cannot delete this path" }, status: :forbidden if File.exist?(path) && path_blocked_for_operations?(path)
|
|
337
|
+
|
|
338
|
+
result = FileOperationService.new(workspace_root).destroy_path(path)
|
|
339
|
+
broadcast_files_changed if result.key?(:type)
|
|
340
|
+
render json: result
|
|
320
341
|
rescue StandardError => e
|
|
321
342
|
render json: { error: e.message }, status: :unprocessable_content
|
|
322
343
|
end
|
|
@@ -333,12 +354,12 @@ module Mbeditor
|
|
|
333
354
|
|
|
334
355
|
results = case language
|
|
335
356
|
when "ruby"
|
|
357
|
+
excl_patterns = Array(Mbeditor.configuration.excluded_paths).map(&:to_s).reject(&:blank?)
|
|
336
358
|
workspace = RubyDefinitionService.call(
|
|
337
359
|
workspace_root,
|
|
338
360
|
symbol,
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
included_dirs: ruby_def_include_dirs
|
|
361
|
+
excluded_paths: excl_patterns,
|
|
362
|
+
included_dirs: ruby_def_include_dirs
|
|
342
363
|
)
|
|
343
364
|
ri = RiDefinitionService.call(symbol)
|
|
344
365
|
workspace + ri
|
|
@@ -351,6 +372,34 @@ module Mbeditor
|
|
|
351
372
|
render json: { error: e.message }, status: :unprocessable_content
|
|
352
373
|
end
|
|
353
374
|
|
|
375
|
+
# GET /mbeditor/js_definition?symbol=ReactWindow
|
|
376
|
+
# Searches workspace JS/JSX/TS/TSX files for global definitions of the named symbol.
|
|
377
|
+
def js_definition
|
|
378
|
+
symbol = params[:symbol].to_s.strip
|
|
379
|
+
return render json: { results: [] } if symbol.blank?
|
|
380
|
+
return render json: { error: "Invalid symbol" }, status: :bad_request \
|
|
381
|
+
unless symbol.match?(/\A[a-zA-Z_$][a-zA-Z0-9_$]{0,59}\z/)
|
|
382
|
+
|
|
383
|
+
results = JsDefinitionService.new(symbol, workspace_root).call
|
|
384
|
+
render json: { results: results }
|
|
385
|
+
rescue StandardError => e
|
|
386
|
+
render json: { error: e.message }, status: :unprocessable_content
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
# GET /mbeditor/js_members?symbol=ReactWindow
|
|
390
|
+
# Searches workspace JS/JSX/TS/TSX files for properties/methods of the named global.
|
|
391
|
+
def js_members
|
|
392
|
+
symbol = params[:symbol].to_s.strip
|
|
393
|
+
return render json: { members: [] } if symbol.blank?
|
|
394
|
+
return render json: { error: "Invalid symbol" }, status: :bad_request \
|
|
395
|
+
unless symbol.match?(/\A[a-zA-Z_$][a-zA-Z0-9_$]{0,59}\z/)
|
|
396
|
+
|
|
397
|
+
members = JsMembersService.new(symbol, workspace_root).call
|
|
398
|
+
render json: { symbol: symbol, members: members }
|
|
399
|
+
rescue StandardError => e
|
|
400
|
+
render json: { error: e.message }, status: :unprocessable_content
|
|
401
|
+
end
|
|
402
|
+
|
|
354
403
|
# GET /mbeditor/module_members?name=ArticlesHelper
|
|
355
404
|
# Returns methods defined in the workspace file that defines the named module/class.
|
|
356
405
|
def module_members
|
|
@@ -358,11 +407,11 @@ module Mbeditor
|
|
|
358
407
|
return render json: { error: "Invalid name" }, status: :bad_request \
|
|
359
408
|
unless name.match?(/\A[A-Z][a-zA-Z0-9_]*\z/)
|
|
360
409
|
|
|
410
|
+
excl_patterns = Array(Mbeditor.configuration.excluded_paths).map(&:to_s).reject(&:blank?)
|
|
361
411
|
file = RubyDefinitionService.module_defined_in(
|
|
362
412
|
workspace_root, name,
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
included_dirs: ruby_def_include_dirs
|
|
413
|
+
excluded_paths: excl_patterns,
|
|
414
|
+
included_dirs: ruby_def_include_dirs
|
|
366
415
|
)
|
|
367
416
|
return render json: { name: name, methods: [] } unless file
|
|
368
417
|
|
|
@@ -381,20 +430,13 @@ module Mbeditor
|
|
|
381
430
|
path = resolve_path(params[:path])
|
|
382
431
|
return render json: { error: "Forbidden" }, status: :forbidden unless path
|
|
383
432
|
|
|
384
|
-
|
|
385
|
-
# Fast no-op on subsequent calls (mtime checks only).
|
|
386
|
-
RubyDefinitionService.scan(workspace_root,
|
|
387
|
-
excluded_dirnames: excluded_dirnames,
|
|
388
|
-
excluded_paths: excluded_paths,
|
|
389
|
-
included_dirs: ruby_def_include_dirs)
|
|
390
|
-
|
|
433
|
+
excl_patterns = Array(Mbeditor.configuration.excluded_paths).map(&:to_s).reject(&:blank?)
|
|
391
434
|
module_names = RubyDefinitionService.includes_in_file(path)
|
|
392
435
|
includes = module_names.filter_map do |mod_name|
|
|
393
436
|
mod_file = RubyDefinitionService.module_defined_in(
|
|
394
437
|
workspace_root, mod_name,
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
included_dirs: ruby_def_include_dirs
|
|
438
|
+
excluded_paths: excl_patterns,
|
|
439
|
+
included_dirs: ruby_def_include_dirs
|
|
398
440
|
)
|
|
399
441
|
next unless mod_file
|
|
400
442
|
|
|
@@ -409,22 +451,6 @@ module Mbeditor
|
|
|
409
451
|
render json: { error: e.message }, status: :unprocessable_content
|
|
410
452
|
end
|
|
411
453
|
|
|
412
|
-
# GET /mbeditor/unused_methods?path=app/models/article.rb
|
|
413
|
-
# Returns method names defined in the file that have no call-sites in the workspace.
|
|
414
|
-
def unused_methods
|
|
415
|
-
path = resolve_path(params[:path])
|
|
416
|
-
return render json: { error: "Forbidden" }, status: :forbidden unless path
|
|
417
|
-
|
|
418
|
-
unused = UnusedMethodsService.call(
|
|
419
|
-
workspace_root, path,
|
|
420
|
-
excluded_dirnames: excluded_dirnames,
|
|
421
|
-
excluded_paths: excluded_paths
|
|
422
|
-
)
|
|
423
|
-
render json: { unused: unused }
|
|
424
|
-
rescue StandardError => e
|
|
425
|
-
render json: { error: e.message }, status: :unprocessable_content
|
|
426
|
-
end
|
|
427
|
-
|
|
428
454
|
# GET /mbeditor/search?q=...&offset=0&limit=50®ex=false&match_case=false&whole_word=false
|
|
429
455
|
def search
|
|
430
456
|
query = params[:q].to_s.strip
|
|
@@ -439,9 +465,9 @@ module Mbeditor
|
|
|
439
465
|
return render json: { error: "Query too long" }, status: :bad_request if query.length > 500
|
|
440
466
|
|
|
441
467
|
# On first page, count total matches in parallel with fetching results.
|
|
442
|
-
count_thread = offset == 0 ? Thread.new {
|
|
468
|
+
count_thread = offset == 0 ? Thread.new { SearchReplaceService.count(workspace_root, query, use_regex: use_regex, match_case: match_case, whole_word: whole_word, excluded_paths: Mbeditor.configuration.excluded_paths) } : nil
|
|
443
469
|
|
|
444
|
-
results =
|
|
470
|
+
results = SearchReplaceService.search(workspace_root, query, limit: needed, use_regex: use_regex, match_case: match_case, whole_word: whole_word, excluded_paths: Mbeditor.configuration.excluded_paths)
|
|
445
471
|
has_more = results.length > offset + limit
|
|
446
472
|
response = { results: results[offset, limit] || [], has_more: has_more }
|
|
447
473
|
if count_thread
|
|
@@ -456,8 +482,6 @@ module Mbeditor
|
|
|
456
482
|
render json: { error: e.message }, status: :unprocessable_content
|
|
457
483
|
end
|
|
458
484
|
|
|
459
|
-
MAX_REPLACE_FILES = 500
|
|
460
|
-
|
|
461
485
|
# POST /mbeditor/replace_in_files
|
|
462
486
|
# Replaces a string/pattern across all matching files in the workspace.
|
|
463
487
|
# Returns { replaced_count:, files_affected:[], errors:[], partial: }
|
|
@@ -471,85 +495,17 @@ module Mbeditor
|
|
|
471
495
|
return render json: { error: "Query is required" }, status: :bad_request if query.blank?
|
|
472
496
|
return render json: { error: "Query too long" }, status: :bad_request if query.length > 500
|
|
473
497
|
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
# Fix 3: Cap the number of files to process
|
|
480
|
-
if file_paths.length > MAX_REPLACE_FILES
|
|
481
|
-
return render json: { error: "Too many files matched (#{file_paths.length}). Narrow your search." }, status: :unprocessable_entity
|
|
482
|
-
end
|
|
483
|
-
|
|
484
|
-
replaced_count = 0
|
|
485
|
-
files_affected = []
|
|
486
|
-
errors = []
|
|
487
|
-
|
|
488
|
-
# Build the Ruby Regexp to use for gsub
|
|
489
|
-
begin
|
|
490
|
-
pattern = if use_regex
|
|
491
|
-
flags = match_case ? 0 : Regexp::IGNORECASE
|
|
492
|
-
Regexp.new(whole_word ? "\\b(?:#{query})\\b" : query, flags)
|
|
493
|
-
else
|
|
494
|
-
flags = match_case ? 0 : Regexp::IGNORECASE
|
|
495
|
-
Regexp.new(whole_word ? "\\b#{Regexp.escape(query)}\\b" : Regexp.escape(query), flags)
|
|
496
|
-
end
|
|
497
|
-
rescue RegexpError => e
|
|
498
|
-
return render json: { error: "Invalid regex: #{e.message}" }, status: :bad_request
|
|
499
|
-
end
|
|
500
|
-
|
|
501
|
-
file_paths.each do |rel_path|
|
|
502
|
-
full_path = resolve_path(rel_path)
|
|
503
|
-
unless full_path
|
|
504
|
-
errors << { file: rel_path, error: "Forbidden" }
|
|
505
|
-
next
|
|
506
|
-
end
|
|
507
|
-
|
|
508
|
-
# Fix 2: Check path_blocked_for_operations?
|
|
509
|
-
if path_blocked_for_operations?(full_path)
|
|
510
|
-
errors << { file: rel_path, error: "Forbidden" }
|
|
511
|
-
next
|
|
512
|
-
end
|
|
513
|
-
|
|
514
|
-
unless File.file?(full_path)
|
|
515
|
-
errors << { file: rel_path, error: "File not found" }
|
|
516
|
-
next
|
|
517
|
-
end
|
|
518
|
-
if File.size(full_path) > MAX_OPEN_FILE_SIZE_BYTES
|
|
519
|
-
errors << { file: rel_path, error: "File too large" }
|
|
520
|
-
next
|
|
521
|
-
end
|
|
498
|
+
result = SearchReplaceService.replace(
|
|
499
|
+
workspace_root, query, replacement,
|
|
500
|
+
use_regex: use_regex, match_case: match_case, whole_word: whole_word,
|
|
501
|
+
excluded_paths: Mbeditor.configuration.excluded_paths
|
|
502
|
+
)
|
|
522
503
|
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
replacements_in_file = content.scan(pattern).length
|
|
528
|
-
new_content = content.gsub(pattern, replacement)
|
|
529
|
-
|
|
530
|
-
# Fix 4: Use new_content != content instead of delta logic
|
|
531
|
-
if new_content != content
|
|
532
|
-
File.binwrite(full_path, new_content.encode("UTF-8", invalid: :replace, undef: :replace))
|
|
533
|
-
files_affected << rel_path
|
|
534
|
-
replaced_count += replacements_in_file
|
|
535
|
-
end
|
|
536
|
-
end
|
|
537
|
-
rescue Timeout::Error
|
|
538
|
-
errors << { file: rel_path, error: "Timed out processing file" }
|
|
539
|
-
next
|
|
540
|
-
rescue StandardError => e
|
|
541
|
-
errors << { file: rel_path, error: e.message }
|
|
542
|
-
next
|
|
543
|
-
end
|
|
504
|
+
if result.key?(:error)
|
|
505
|
+
render json: { error: result[:error] }, status: :unprocessable_entity
|
|
506
|
+
else
|
|
507
|
+
render json: result
|
|
544
508
|
end
|
|
545
|
-
|
|
546
|
-
# Fix 5: Surface partial failure
|
|
547
|
-
render json: {
|
|
548
|
-
replaced_count: replaced_count,
|
|
549
|
-
files_affected: files_affected,
|
|
550
|
-
errors: errors,
|
|
551
|
-
partial: errors.any? && files_affected.any?
|
|
552
|
-
}
|
|
553
509
|
rescue StandardError => e
|
|
554
510
|
render json: { error: e.message }, status: :unprocessable_content
|
|
555
511
|
end
|
|
@@ -558,7 +514,7 @@ module Mbeditor
|
|
|
558
514
|
def git_status
|
|
559
515
|
output, _err, status = Open3.capture3("git", "-C", workspace_root.to_s, "status", "--porcelain")
|
|
560
516
|
branch = GitService.current_branch(workspace_root.to_s) || ""
|
|
561
|
-
files = parse_porcelain_status(output)
|
|
517
|
+
files = GitService.parse_porcelain_status(output)
|
|
562
518
|
render json: { ok: status.success?, files: files, branch: branch }
|
|
563
519
|
rescue StandardError => e
|
|
564
520
|
render json: { error: e.message }, status: :unprocessable_content
|
|
@@ -566,120 +522,7 @@ module Mbeditor
|
|
|
566
522
|
|
|
567
523
|
# GET /mbeditor/git_info
|
|
568
524
|
def git_info
|
|
569
|
-
|
|
570
|
-
cached = cached_git_info(repo)
|
|
571
|
-
return render json: cached if cached
|
|
572
|
-
|
|
573
|
-
branch = GitService.current_branch(repo)
|
|
574
|
-
unless branch
|
|
575
|
-
return render json: { ok: false, error: "Unable to determine current branch" }, status: :unprocessable_content
|
|
576
|
-
end
|
|
577
|
-
|
|
578
|
-
# Wave 1: all independent git reads run in parallel
|
|
579
|
-
status_t = Thread.new { Open3.capture3("git", "-C", repo, "status", "--porcelain") }
|
|
580
|
-
numstat_t = Thread.new { Open3.capture3("git", "-C", repo, "diff", "--numstat", "HEAD") }
|
|
581
|
-
upstream_t = Thread.new { Open3.capture3("git", "-C", repo, "rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}") }
|
|
582
|
-
base_t = Thread.new { GitService.find_branch_base(repo, branch) }
|
|
583
|
-
|
|
584
|
-
working_output, _err, working_status = status_t.value
|
|
585
|
-
working_tree = working_status.success? ? parse_porcelain_status(working_output) : []
|
|
586
|
-
|
|
587
|
-
numstat_out = numstat_t.value.first
|
|
588
|
-
numstat_map = GitService.parse_numstat(numstat_out)
|
|
589
|
-
working_tree = working_tree.map { |f| f.merge(numstat_map.fetch(f[:path], {})) }
|
|
590
|
-
|
|
591
|
-
upstream_output, _err, upstream_status = upstream_t.value
|
|
592
|
-
upstream_branch = upstream_status.success? ? upstream_output.strip : nil
|
|
593
|
-
upstream_branch = nil unless upstream_branch&.match?(%r{\A[\w./-]+\z})
|
|
594
|
-
|
|
595
|
-
# Determine the branch's fork point relative to a base branch (develop/main/master).
|
|
596
|
-
# This ensures History and Changes only show work unique to this branch.
|
|
597
|
-
base_sha, base_ref = base_t.value
|
|
598
|
-
|
|
599
|
-
ahead_count = 0
|
|
600
|
-
behind_count = 0
|
|
601
|
-
unpushed_files = []
|
|
602
|
-
unpushed_commits = []
|
|
603
|
-
diff_base = base_sha || upstream_branch
|
|
604
|
-
|
|
605
|
-
# Wave 2: conditional parallel reads that depend on Wave 1 results
|
|
606
|
-
wave2 = {}
|
|
607
|
-
wave2[:counts] = Thread.new { Open3.capture3("git", "-C", repo, "rev-list", "--left-right", "--count", "HEAD...#{upstream_branch}") } if upstream_branch.present?
|
|
608
|
-
wave2[:unp_log] = Thread.new { Open3.capture3("git", "-C", repo, "log", "#{upstream_branch}..HEAD", "--pretty=format:%H%x1f%s%x1f%an%x1f%aI%x1e") } if upstream_branch.present?
|
|
609
|
-
wave2[:diff_name] = Thread.new { Open3.capture3("git", "-C", repo, "diff", "--name-status", "#{diff_base}..HEAD") } if diff_base.present?
|
|
610
|
-
wave2[:diff_num] = Thread.new { Open3.capture3("git", "-C", repo, "diff", "--numstat", "#{diff_base}..HEAD") } if diff_base.present?
|
|
611
|
-
wave2[:branch_log] = Thread.new do
|
|
612
|
-
if base_sha
|
|
613
|
-
Open3.capture3("git", "-C", repo, "log", "--first-parent", "#{base_sha}..HEAD",
|
|
614
|
-
"--pretty=format:%H%x1f%s%x1f%an%x1f%aI%x1e")
|
|
615
|
-
else
|
|
616
|
-
Open3.capture3("git", "-C", repo, "log", "--first-parent", branch, "-n", "100",
|
|
617
|
-
"--pretty=format:%H%x1f%s%x1f%an%x1f%aI%x1e")
|
|
618
|
-
end
|
|
619
|
-
end
|
|
620
|
-
|
|
621
|
-
wave2.each_value(&:join)
|
|
622
|
-
|
|
623
|
-
if (ct = wave2[:counts])
|
|
624
|
-
counts_output, _err, counts_status = ct.value
|
|
625
|
-
if counts_status.success?
|
|
626
|
-
ahead_str, behind_str = counts_output.strip.split("\t", 2)
|
|
627
|
-
ahead_count = ahead_str.to_i
|
|
628
|
-
behind_count = behind_str.to_i
|
|
629
|
-
end
|
|
630
|
-
end
|
|
631
|
-
|
|
632
|
-
if (ul = wave2[:unp_log])
|
|
633
|
-
unpushed_log_output, _err, unpushed_log_status = ul.value
|
|
634
|
-
unpushed_commits = GitService.parse_git_log(unpushed_log_output) if unpushed_log_status.success?
|
|
635
|
-
end
|
|
636
|
-
|
|
637
|
-
if (dn = wave2[:diff_name]) && (dnum = wave2[:diff_num])
|
|
638
|
-
diff_name_out, _err, diff_name_status = dn.value
|
|
639
|
-
if diff_name_status.success?
|
|
640
|
-
unpushed_files = parse_name_status(diff_name_out)
|
|
641
|
-
unp_numstat_out = dnum.value.first
|
|
642
|
-
unp_numstat_map = GitService.parse_numstat(unp_numstat_out)
|
|
643
|
-
unpushed_files = unpushed_files.map { |f| f.merge(unp_numstat_map.fetch(f[:path], {})) }
|
|
644
|
-
end
|
|
645
|
-
end
|
|
646
|
-
|
|
647
|
-
branch_log_output, _err, branch_log_status = wave2[:branch_log].value
|
|
648
|
-
branch_commits = branch_log_status.success? ? GitService.parse_git_log(branch_log_output) : []
|
|
649
|
-
|
|
650
|
-
redmine_ticket_id = nil
|
|
651
|
-
if Mbeditor.configuration.redmine_enabled
|
|
652
|
-
if Mbeditor.configuration.redmine_ticket_source == :branch
|
|
653
|
-
m = branch.match(/\A(\d+)/)
|
|
654
|
-
redmine_ticket_id = m[1] if m
|
|
655
|
-
else
|
|
656
|
-
branch_commits.each do |commit|
|
|
657
|
-
m = commit["title"]&.match(/#(\d+)/)
|
|
658
|
-
if m
|
|
659
|
-
redmine_ticket_id = m[1]
|
|
660
|
-
break
|
|
661
|
-
end
|
|
662
|
-
end
|
|
663
|
-
end
|
|
664
|
-
end
|
|
665
|
-
|
|
666
|
-
payload = {
|
|
667
|
-
ok: true,
|
|
668
|
-
branch: branch,
|
|
669
|
-
upstreamBranch: upstream_branch,
|
|
670
|
-
ahead: ahead_count,
|
|
671
|
-
behind: behind_count,
|
|
672
|
-
workingTree: working_tree,
|
|
673
|
-
unpushedFiles: unpushed_files,
|
|
674
|
-
unpushedCommits: unpushed_commits,
|
|
675
|
-
branchCommits: branch_commits,
|
|
676
|
-
branchBaseRef: base_ref,
|
|
677
|
-
redmineTicketId: redmine_ticket_id
|
|
678
|
-
}
|
|
679
|
-
store_git_info(repo, payload)
|
|
680
|
-
render json: payload
|
|
681
|
-
rescue StandardError => e
|
|
682
|
-
render json: { ok: false, error: e.message }, status: :unprocessable_content
|
|
525
|
+
render json: GitInfoService.call(workspace_root.to_s)
|
|
683
526
|
end
|
|
684
527
|
|
|
685
528
|
# GET /mbeditor/monaco-editor/*asset_path — serve packaged Monaco files
|
|
@@ -702,7 +545,8 @@ module Mbeditor
|
|
|
702
545
|
|
|
703
546
|
# GET /mbeditor/manifest.webmanifest — PWA manifest
|
|
704
547
|
def pwa_manifest
|
|
705
|
-
|
|
548
|
+
raw = root_path.chomp("/")
|
|
549
|
+
base = raw.start_with?("/") || raw.empty? ? raw : "/#{raw}"
|
|
706
550
|
manifest = {
|
|
707
551
|
name: "Mbeditor — #{Rails.root.basename}",
|
|
708
552
|
short_name: "Mbeditor",
|
|
@@ -751,11 +595,11 @@ module Mbeditor
|
|
|
751
595
|
path = resolve_path(params[:path])
|
|
752
596
|
return render json: { error: "Forbidden" }, status: :forbidden unless path
|
|
753
597
|
|
|
754
|
-
filename =
|
|
598
|
+
filename = path.to_s
|
|
755
599
|
code = params[:code] || File.read(path)
|
|
756
600
|
|
|
757
|
-
if filename.end_with?('.haml')
|
|
758
|
-
unless
|
|
601
|
+
if File.basename(filename).end_with?('.haml')
|
|
602
|
+
unless AvailabilityProbe.haml_lint(workspace_root)
|
|
759
603
|
return render json: { error: "haml-lint not available", markers: [] }, status: :unprocessable_content
|
|
760
604
|
end
|
|
761
605
|
|
|
@@ -763,7 +607,7 @@ module Mbeditor
|
|
|
763
607
|
return render json: { markers: markers }
|
|
764
608
|
end
|
|
765
609
|
|
|
766
|
-
cmd = rubocop_command + ["--no-server", "--stdin", filename, "--format", "json", "--no-color"
|
|
610
|
+
cmd = AvailabilityProbe.rubocop_command(workspace_root) + ["--no-server", "--stdin", filename, "--format", "json", "--no-color"]
|
|
767
611
|
env = { 'RUBOCOP_CACHE_ROOT' => File.join(Dir.tmpdir, 'rubocop') }
|
|
768
612
|
output = run_with_timeout(env, cmd, stdin_data: code)
|
|
769
613
|
|
|
@@ -824,7 +668,7 @@ module Mbeditor
|
|
|
824
668
|
f.flush
|
|
825
669
|
tmpfile = f.path
|
|
826
670
|
|
|
827
|
-
cmd = rubocop_command + ["--no-server", "-A", "--no-color", tmpfile]
|
|
671
|
+
cmd = AvailabilityProbe.rubocop_command(workspace_root) + ["--no-server", "-A", "--no-color", tmpfile]
|
|
828
672
|
env = { 'RUBOCOP_CACHE_ROOT' => File.join(Dir.tmpdir, 'rubocop') }
|
|
829
673
|
_out, _err, status = Open3.capture3(env, *cmd)
|
|
830
674
|
|
|
@@ -867,6 +711,46 @@ module Mbeditor
|
|
|
867
711
|
render json: { error: e.message, ok: false }, status: :unprocessable_content
|
|
868
712
|
end
|
|
869
713
|
|
|
714
|
+
# GET /mbeditor/client_config — returns client-side configuration values
|
|
715
|
+
def client_config
|
|
716
|
+
render json: {
|
|
717
|
+
related_files_custom_paths: Array(Mbeditor.configuration.related_files_custom_paths)
|
|
718
|
+
}
|
|
719
|
+
end
|
|
720
|
+
|
|
721
|
+
# GET /mbeditor/related_files?path=...
|
|
722
|
+
def related_files
|
|
723
|
+
path = resolve_path(params[:path])
|
|
724
|
+
return render json: {}, status: :bad_request unless path
|
|
725
|
+
rel = relative_path(path)
|
|
726
|
+
custom = Array(Mbeditor.configuration.related_files_custom_paths)
|
|
727
|
+
result = RailsRelatedFilesService.find(workspace_root.to_s, rel, custom_paths: custom)
|
|
728
|
+
render json: result
|
|
729
|
+
end
|
|
730
|
+
|
|
731
|
+
# GET /mbeditor/changelog — serves the gem's CHANGELOG.md for the in-editor changelog tab.
|
|
732
|
+
def changelog
|
|
733
|
+
path = Mbeditor::Engine.root.join("CHANGELOG.md")
|
|
734
|
+
content = path.exist? ? path.read(encoding: "utf-8") : ""
|
|
735
|
+
render json: { content: content, version: Mbeditor::VERSION }
|
|
736
|
+
end
|
|
737
|
+
|
|
738
|
+
# GET /mbeditor/model_schema?model=User
|
|
739
|
+
# Returns the db/schema.rb table definition for the named model as JSON.
|
|
740
|
+
# 404 when the schema file is missing or the table is not found.
|
|
741
|
+
def model_schema
|
|
742
|
+
model_name = params[:model].to_s.strip
|
|
743
|
+
return render json: { error: "model required" }, status: :bad_request if model_name.blank?
|
|
744
|
+
|
|
745
|
+
schema = SchemaService.new(model_name, workspace_root.to_s).call
|
|
746
|
+
if schema
|
|
747
|
+
render json: schema
|
|
748
|
+
else
|
|
749
|
+
hint = " (Check if the model has a custom table_name or if db/schema.rb exists)"
|
|
750
|
+
render json: { error: "No schema found for '#{model_name}'#{hint}" }, status: :not_found
|
|
751
|
+
end
|
|
752
|
+
end
|
|
753
|
+
|
|
870
754
|
# POST /mbeditor/format — rubocop -A on buffer content; returns corrected content WITHOUT saving to disk
|
|
871
755
|
#
|
|
872
756
|
# Accepts the current buffer content as `code` and formats it using a
|
|
@@ -887,7 +771,7 @@ module Mbeditor
|
|
|
887
771
|
f.flush
|
|
888
772
|
tmpfile = f.path
|
|
889
773
|
|
|
890
|
-
cmd = rubocop_command + ["--no-server", "-A", "--no-color", tmpfile]
|
|
774
|
+
cmd = AvailabilityProbe.rubocop_command(workspace_root) + ["--no-server", "-A", "--no-color", tmpfile]
|
|
891
775
|
env = { 'RUBOCOP_CACHE_ROOT' => File.join(Dir.tmpdir, 'rubocop') }
|
|
892
776
|
_out, _err, status = Open3.capture3(env, *cmd)
|
|
893
777
|
unless status.success? || status.exitstatus == 1
|
|
@@ -903,10 +787,44 @@ module Mbeditor
|
|
|
903
787
|
|
|
904
788
|
private
|
|
905
789
|
|
|
790
|
+
def history_file_path(branch, rel_path)
|
|
791
|
+
branch_hash = Digest::SHA256.hexdigest(branch.to_s)[0, 16]
|
|
792
|
+
file_hash = Digest::SHA256.hexdigest(rel_path.to_s)[0, 16]
|
|
793
|
+
workspace_root.join('tmp', 'mbeditor_history', "#{branch_hash}_#{file_hash}.json")
|
|
794
|
+
end
|
|
795
|
+
|
|
796
|
+
def compact_history_ops(base, ops)
|
|
797
|
+
text = base.to_s
|
|
798
|
+
ops.each do |op|
|
|
799
|
+
sl, sc, el, ec, ins = op[0].to_i, op[1].to_i, op[2].to_i, op[3].to_i, op[4].to_s
|
|
800
|
+
lines = text.split("\n", -1)
|
|
801
|
+
sl0 = [[sl - 1, 0].max, [lines.length - 1, 0].max].min
|
|
802
|
+
el0 = [[el - 1, 0].max, [lines.length - 1, 0].max].min
|
|
803
|
+
sc0 = sc - 1
|
|
804
|
+
ec0 = ec - 1
|
|
805
|
+
prefix = (lines[sl0] || '')[0, sc0] || ''
|
|
806
|
+
suffix = (lines[el0] || '')[ec0..] || ''
|
|
807
|
+
ins_lines = ins.split("\n", -1)
|
|
808
|
+
new_seg = if ins_lines.length <= 1
|
|
809
|
+
[prefix + (ins_lines[0] || '') + suffix]
|
|
810
|
+
else
|
|
811
|
+
[prefix + ins_lines[0]] + ins_lines[1..-2] + [ins_lines[-1] + suffix]
|
|
812
|
+
end
|
|
813
|
+
text = (lines[0...sl0] + new_seg + lines[(el0 + 1)..]).join("\n")
|
|
814
|
+
end
|
|
815
|
+
text
|
|
816
|
+
rescue StandardError
|
|
817
|
+
base.to_s
|
|
818
|
+
end
|
|
819
|
+
|
|
906
820
|
def broadcast_files_changed
|
|
907
821
|
root = workspace_root.to_s
|
|
908
|
-
|
|
909
|
-
|
|
822
|
+
FileTreeService.invalidate(root)
|
|
823
|
+
Thread.new do
|
|
824
|
+
GitInfoService.invalidate(root)
|
|
825
|
+
rescue => e
|
|
826
|
+
Rails.logger.warn("[mbeditor] GitInfoService.invalidate failed: #{e}")
|
|
827
|
+
end
|
|
910
828
|
|
|
911
829
|
return unless defined?(ActionCable.server)
|
|
912
830
|
|
|
@@ -915,11 +833,15 @@ module Mbeditor
|
|
|
915
833
|
# Never let a broadcast failure affect the HTTP response
|
|
916
834
|
end
|
|
917
835
|
|
|
836
|
+
def editor_state_service
|
|
837
|
+
@editor_state_service ||= EditorStateService.new(workspace_root)
|
|
838
|
+
end
|
|
839
|
+
|
|
918
840
|
def sanitize_branch_name(branch)
|
|
919
841
|
return nil if branch.blank?
|
|
920
842
|
str = branch.to_s.strip
|
|
921
843
|
return nil if str.include?('..')
|
|
922
|
-
str.match?(SAFE_BRANCH_NAME) ? str : nil
|
|
844
|
+
str.match?(EditorStateService::SAFE_BRANCH_NAME) ? str : nil
|
|
923
845
|
end
|
|
924
846
|
|
|
925
847
|
def action_cable_enabled?
|
|
@@ -961,173 +883,18 @@ module Mbeditor
|
|
|
961
883
|
rel = relative_path(full_path)
|
|
962
884
|
return true if rel.blank?
|
|
963
885
|
|
|
964
|
-
|
|
965
|
-
end
|
|
966
|
-
|
|
967
|
-
# Stream search results using popen so we can stop reading early once we
|
|
968
|
-
# have collected `limit` matches (avoids buffering the entire rg/grep output
|
|
969
|
-
# in memory when searching large codebases for common tokens).
|
|
970
|
-
def stream_search_results(query, limit, use_regex: false, match_case: false, whole_word: false)
|
|
971
|
-
results = []
|
|
972
|
-
|
|
973
|
-
if RG_AVAILABLE
|
|
974
|
-
args = ["rg", "--json", "--no-ignore"]
|
|
975
|
-
args << "-F" unless use_regex
|
|
976
|
-
args << "--ignore-case" unless match_case
|
|
977
|
-
args << "--word-regexp" if whole_word
|
|
978
|
-
excluded_paths.each { |p| args << "--glob=!#{p}" }
|
|
979
|
-
args += ["--", query, workspace_root.to_s]
|
|
980
|
-
|
|
981
|
-
IO.popen(args, err: File::NULL) do |io|
|
|
982
|
-
io.each_line do |raw|
|
|
983
|
-
break if results.length >= limit
|
|
984
|
-
|
|
985
|
-
begin
|
|
986
|
-
data = JSON.parse(raw)
|
|
987
|
-
rescue JSON::ParserError => e
|
|
988
|
-
Rails.logger.warn("[mbeditor] search: malformed rg JSON line: #{e.message}")
|
|
989
|
-
next
|
|
990
|
-
end
|
|
991
|
-
next unless data["type"] == "match"
|
|
992
|
-
|
|
993
|
-
md = data["data"]
|
|
994
|
-
results << {
|
|
995
|
-
file: relative_path(md.dig("path", "text").to_s),
|
|
996
|
-
line: md.dig("line_number"),
|
|
997
|
-
text: md.dig("lines", "text").to_s.strip
|
|
998
|
-
}
|
|
999
|
-
end
|
|
1000
|
-
end
|
|
1001
|
-
else
|
|
1002
|
-
base_flags = use_regex ? "-E" : "-F"
|
|
1003
|
-
args = ["grep", "-rn", base_flags]
|
|
1004
|
-
args << "-i" unless match_case
|
|
1005
|
-
args << "-w" if whole_word
|
|
1006
|
-
excluded_dirnames.select { |d| d.match?(/\A[\w.\/-]+\z/) }.each { |d| args << "--exclude-dir=#{d}" }
|
|
1007
|
-
args += [query, workspace_root.to_s]
|
|
1008
|
-
|
|
1009
|
-
IO.popen(args, err: File::NULL) do |io|
|
|
1010
|
-
io.each_line do |raw|
|
|
1011
|
-
break if results.length >= limit
|
|
1012
|
-
|
|
1013
|
-
raw.chomp!
|
|
1014
|
-
next unless raw =~ /\A(.+?):(\d+):(.*)\z/
|
|
1015
|
-
|
|
1016
|
-
file_path = Regexp.last_match(1)
|
|
1017
|
-
next unless file_path.start_with?(workspace_root.to_s)
|
|
1018
|
-
|
|
1019
|
-
rel = relative_path(file_path)
|
|
1020
|
-
next if excluded_path?(rel, File.basename(file_path))
|
|
1021
|
-
|
|
1022
|
-
results << {
|
|
1023
|
-
file: rel,
|
|
1024
|
-
line: Regexp.last_match(2).to_i,
|
|
1025
|
-
text: Regexp.last_match(3).strip
|
|
1026
|
-
}
|
|
1027
|
-
end
|
|
1028
|
-
end
|
|
1029
|
-
end
|
|
1030
|
-
|
|
1031
|
-
results
|
|
1032
|
-
end
|
|
1033
|
-
|
|
1034
|
-
# Count total matching lines across the workspace using rg --count (or grep -c).
|
|
1035
|
-
# Fast: rg just counts without extracting context. Runs in a background thread.
|
|
1036
|
-
def count_search_results(query, use_regex: false, match_case: false, whole_word: false)
|
|
1037
|
-
total = 0
|
|
1038
|
-
if RG_AVAILABLE
|
|
1039
|
-
args = ["rg", "--count", "--no-ignore"]
|
|
1040
|
-
args << "-F" unless use_regex
|
|
1041
|
-
args << "--ignore-case" unless match_case
|
|
1042
|
-
args << "--word-regexp" if whole_word
|
|
1043
|
-
excluded_paths.each { |p| args << "--glob=!#{p}" }
|
|
1044
|
-
args += ["--", query, workspace_root.to_s]
|
|
1045
|
-
IO.popen(args, err: File::NULL) do |io|
|
|
1046
|
-
io.each_line { |line| total += line.strip.split(":").last.to_i rescue 0 }
|
|
1047
|
-
end
|
|
1048
|
-
else
|
|
1049
|
-
base_flags = use_regex ? "-E" : "-F"
|
|
1050
|
-
args = ["grep", "-rc", base_flags]
|
|
1051
|
-
args << "-i" unless match_case
|
|
1052
|
-
args << "-w" if whole_word
|
|
1053
|
-
excluded_dirnames.each { |d| args << "--exclude-dir=#{d}" }
|
|
1054
|
-
args += [query, workspace_root.to_s]
|
|
1055
|
-
IO.popen(args, err: File::NULL) do |io|
|
|
1056
|
-
io.each_line { |line| total += line.strip.split(":").last.to_i rescue 0 }
|
|
1057
|
-
end
|
|
1058
|
-
end
|
|
1059
|
-
total
|
|
1060
|
-
rescue StandardError
|
|
1061
|
-
0
|
|
1062
|
-
end
|
|
1063
|
-
|
|
1064
|
-
def build_tree(dir, max_depth: 10, depth: 0)
|
|
1065
|
-
return [] if depth >= max_depth
|
|
1066
|
-
|
|
1067
|
-
entries = Dir.entries(dir).sort.reject { |entry| entry == "." || entry == ".." }
|
|
1068
|
-
entries.filter_map do |name|
|
|
1069
|
-
full = File.join(dir, name)
|
|
1070
|
-
rel = relative_path(full)
|
|
1071
|
-
|
|
1072
|
-
if File.directory?(full)
|
|
1073
|
-
{ name: name, type: "folder", path: rel, children: build_tree(full, depth: depth + 1) }
|
|
1074
|
-
else
|
|
1075
|
-
size = File.size(full) rescue nil
|
|
1076
|
-
{ name: name, type: "file", path: rel, size: size }
|
|
1077
|
-
end
|
|
1078
|
-
end
|
|
1079
|
-
rescue Errno::EACCES
|
|
1080
|
-
[]
|
|
1081
|
-
end
|
|
1082
|
-
|
|
1083
|
-
def excluded_paths
|
|
1084
|
-
Array(Mbeditor.configuration.excluded_paths).map(&:to_s).reject(&:blank?)
|
|
1085
|
-
end
|
|
1086
|
-
|
|
1087
|
-
def excluded_dirnames
|
|
1088
|
-
excluded_paths.filter { |path| !path.include?("/") }
|
|
886
|
+
ExclusionMatcher.new(Mbeditor.configuration.excluded_paths).excluded?(rel)
|
|
1089
887
|
end
|
|
1090
888
|
|
|
1091
889
|
def ruby_def_include_dirs
|
|
1092
890
|
Array(Mbeditor.configuration.ruby_def_include_dirs).map(&:to_s).reject(&:blank?)
|
|
1093
891
|
end
|
|
1094
892
|
|
|
1095
|
-
def excluded_path?(relative_path, name)
|
|
1096
|
-
excluded_paths.any? do |pattern|
|
|
1097
|
-
if pattern.include?("/")
|
|
1098
|
-
relative_path == pattern || relative_path.start_with?("#{pattern}/")
|
|
1099
|
-
else
|
|
1100
|
-
name == pattern || relative_path.split("/").include?(pattern)
|
|
1101
|
-
end
|
|
1102
|
-
end
|
|
1103
|
-
end
|
|
1104
|
-
|
|
1105
893
|
def run_with_timeout(env, cmd, stdin_data:)
|
|
1106
894
|
timeout_seconds = Mbeditor.configuration.lint_timeout&.to_i
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
stdin.write(stdin_data)
|
|
1111
|
-
stdin.close
|
|
1112
|
-
|
|
1113
|
-
timer = if timeout_seconds && timeout_seconds > 0
|
|
1114
|
-
Thread.new do
|
|
1115
|
-
sleep timeout_seconds
|
|
1116
|
-
timed_out = true
|
|
1117
|
-
Process.kill('-KILL', wait_thr.pid)
|
|
1118
|
-
rescue Errno::ESRCH
|
|
1119
|
-
nil
|
|
1120
|
-
end
|
|
1121
|
-
end
|
|
1122
|
-
|
|
1123
|
-
output = stdout.read
|
|
1124
|
-
wait_thr.value
|
|
1125
|
-
timer&.kill
|
|
1126
|
-
end
|
|
1127
|
-
|
|
1128
|
-
raise "RuboCop timed out after #{timeout_seconds} seconds" if timed_out
|
|
1129
|
-
|
|
1130
|
-
output
|
|
895
|
+
timeout = timeout_seconds && timeout_seconds > 0 ? timeout_seconds : nil
|
|
896
|
+
result = ProcessRunner.call(cmd, timeout: timeout, env: env, stdin_data: stdin_data)
|
|
897
|
+
result[:stdout]
|
|
1131
898
|
end
|
|
1132
899
|
|
|
1133
900
|
def cop_severity(severity)
|
|
@@ -1182,143 +949,22 @@ module Mbeditor
|
|
|
1182
949
|
}
|
|
1183
950
|
end
|
|
1184
951
|
|
|
1185
|
-
def rubocop_command
|
|
1186
|
-
command = Mbeditor.configuration.rubocop_command.to_s.strip
|
|
1187
|
-
command = "rubocop" if command.empty?
|
|
1188
|
-
tokens = Shellwords.split(command)
|
|
1189
|
-
|
|
1190
|
-
local_bin = workspace_root.join("bin", "rubocop")
|
|
1191
|
-
return [local_bin.to_s] if tokens == ["rubocop"] && local_bin.exist?
|
|
1192
|
-
|
|
1193
|
-
tokens
|
|
1194
|
-
rescue ArgumentError
|
|
1195
|
-
["rubocop"]
|
|
1196
|
-
end
|
|
1197
|
-
|
|
1198
952
|
def rubocop_config_path
|
|
1199
953
|
candidate = workspace_root.join(".rubocop.yml")
|
|
1200
954
|
candidate.exist? ? ".rubocop.yml" : nil
|
|
1201
955
|
end
|
|
1202
956
|
|
|
1203
|
-
PROBE_MUTEX = Mutex.new
|
|
1204
|
-
GIT_INFO_MUTEX = Mutex.new
|
|
1205
|
-
FILE_TREE_MUTEX = Mutex.new
|
|
1206
|
-
private_constant :PROBE_MUTEX, :GIT_INFO_MUTEX, :FILE_TREE_MUTEX
|
|
1207
|
-
|
|
1208
|
-
def rubocop_available?
|
|
1209
|
-
key = Mbeditor.configuration.rubocop_command.to_s
|
|
1210
|
-
probe_cached(:@rubocop_available_cache, key) do
|
|
1211
|
-
_out, _err, status = Open3.capture3(*rubocop_command, "--version")
|
|
1212
|
-
status.success?
|
|
1213
|
-
end
|
|
1214
|
-
end
|
|
1215
|
-
|
|
1216
|
-
def haml_lint_available?
|
|
1217
|
-
cmd = haml_lint_command
|
|
1218
|
-
key = cmd.join(" ")
|
|
1219
|
-
probe_cached(:@haml_lint_available_cache, key) do
|
|
1220
|
-
_out, _err, status = Open3.capture3(*cmd, "--version")
|
|
1221
|
-
status.success?
|
|
1222
|
-
end
|
|
1223
|
-
end
|
|
1224
|
-
|
|
1225
|
-
def git_available?
|
|
1226
|
-
key = workspace_root.to_s
|
|
1227
|
-
probe_cached(:@git_available_cache, key) do
|
|
1228
|
-
_out, _err, status = Open3.capture3("git", "-C", key, "rev-parse", "--is-inside-work-tree")
|
|
1229
|
-
status.success?
|
|
1230
|
-
end
|
|
1231
|
-
end
|
|
1232
|
-
|
|
1233
|
-
def cached_git_info(repo, ttl: 5)
|
|
1234
|
-
GIT_INFO_MUTEX.synchronize do
|
|
1235
|
-
cache = self.class.instance_variable_get(:@git_info_cache) || {}
|
|
1236
|
-
entry = cache[repo]
|
|
1237
|
-
return entry[:data] if entry && (Process.clock_gettime(Process::CLOCK_MONOTONIC) - entry[:ts]) < ttl
|
|
1238
|
-
end
|
|
1239
|
-
nil
|
|
1240
|
-
end
|
|
1241
|
-
|
|
1242
|
-
def store_git_info(repo, data)
|
|
1243
|
-
GIT_INFO_MUTEX.synchronize do
|
|
1244
|
-
cache = self.class.instance_variable_get(:@git_info_cache) || {}
|
|
1245
|
-
cache[repo] = { ts: Process.clock_gettime(Process::CLOCK_MONOTONIC), data: data }
|
|
1246
|
-
self.class.instance_variable_set(:@git_info_cache, cache)
|
|
1247
|
-
end
|
|
1248
|
-
end
|
|
1249
|
-
|
|
1250
|
-
def invalidate_git_info_cache(repo)
|
|
1251
|
-
GIT_INFO_MUTEX.synchronize do
|
|
1252
|
-
cache = self.class.instance_variable_get(:@git_info_cache) || {}
|
|
1253
|
-
cache.delete(repo)
|
|
1254
|
-
self.class.instance_variable_set(:@git_info_cache, cache)
|
|
1255
|
-
end
|
|
1256
|
-
end
|
|
1257
|
-
|
|
1258
|
-
def cached_file_tree(root, ttl: 15)
|
|
1259
|
-
FILE_TREE_MUTEX.synchronize do
|
|
1260
|
-
cache = self.class.instance_variable_get(:@file_tree_cache) || {}
|
|
1261
|
-
entry = cache[root]
|
|
1262
|
-
return entry[:data] if entry && (Process.clock_gettime(Process::CLOCK_MONOTONIC) - entry[:ts]) < ttl
|
|
1263
|
-
end
|
|
1264
|
-
nil
|
|
1265
|
-
end
|
|
1266
|
-
|
|
1267
|
-
def store_file_tree(root, data)
|
|
1268
|
-
FILE_TREE_MUTEX.synchronize do
|
|
1269
|
-
cache = self.class.instance_variable_get(:@file_tree_cache) || {}
|
|
1270
|
-
cache[root] = { ts: Process.clock_gettime(Process::CLOCK_MONOTONIC), data: data }
|
|
1271
|
-
self.class.instance_variable_set(:@file_tree_cache, cache)
|
|
1272
|
-
end
|
|
1273
|
-
end
|
|
1274
|
-
|
|
1275
|
-
def invalidate_file_tree_cache(root)
|
|
1276
|
-
FILE_TREE_MUTEX.synchronize do
|
|
1277
|
-
cache = self.class.instance_variable_get(:@file_tree_cache) || {}
|
|
1278
|
-
cache.delete(root)
|
|
1279
|
-
self.class.instance_variable_set(:@file_tree_cache, cache)
|
|
1280
|
-
end
|
|
1281
|
-
end
|
|
1282
|
-
|
|
1283
|
-
def probe_cached(ivar, key, &block)
|
|
1284
|
-
PROBE_MUTEX.synchronize do
|
|
1285
|
-
cache = self.class.instance_variable_get(ivar) ||
|
|
1286
|
-
self.class.instance_variable_set(ivar, {})
|
|
1287
|
-
unless cache.key?(key)
|
|
1288
|
-
cache[key] = begin
|
|
1289
|
-
block.call
|
|
1290
|
-
rescue StandardError
|
|
1291
|
-
false
|
|
1292
|
-
end
|
|
1293
|
-
end
|
|
1294
|
-
cache[key]
|
|
1295
|
-
end
|
|
1296
|
-
end
|
|
1297
|
-
|
|
1298
|
-
alias git_blame_available? git_available?
|
|
1299
|
-
|
|
1300
957
|
def test_available?
|
|
1301
958
|
root = workspace_root.to_s
|
|
1302
959
|
File.directory?(File.join(root, "test")) || File.directory?(File.join(root, "spec"))
|
|
1303
960
|
end
|
|
1304
961
|
|
|
1305
|
-
def haml_lint_command
|
|
1306
|
-
workspace_bin = workspace_root.join("bin", "haml-lint")
|
|
1307
|
-
return [workspace_bin.to_s] if workspace_bin.exist?
|
|
1308
|
-
|
|
1309
|
-
begin
|
|
1310
|
-
[Gem.bin_path("haml_lint", "haml-lint")]
|
|
1311
|
-
rescue Gem::Exception, Gem::GemNotFoundException
|
|
1312
|
-
["haml-lint"]
|
|
1313
|
-
end
|
|
1314
|
-
end
|
|
1315
|
-
|
|
1316
962
|
def run_haml_lint(code)
|
|
1317
963
|
markers = []
|
|
1318
964
|
Tempfile.create(["mbeditor_haml", ".haml"]) do |f|
|
|
1319
965
|
f.write(code)
|
|
1320
966
|
f.flush
|
|
1321
|
-
output, _err, _status = Open3.capture3(*haml_lint_command, "--reporter", "json", "--no-color", f.path)
|
|
967
|
+
output, _err, _status = Open3.capture3(*AvailabilityProbe.haml_lint_command(workspace_root), "--reporter", "json", "--no-color", f.path)
|
|
1322
968
|
idx = output.index("{")
|
|
1323
969
|
result = idx ? JSON.parse(output[idx..]) : {}
|
|
1324
970
|
result = {} unless result.is_a?(Hash)
|
|
@@ -1345,30 +991,6 @@ module Mbeditor
|
|
|
1345
991
|
end
|
|
1346
992
|
end
|
|
1347
993
|
|
|
1348
|
-
def parse_porcelain_status(output)
|
|
1349
|
-
output.lines.filter_map do |line|
|
|
1350
|
-
next if line.length < 4
|
|
1351
|
-
|
|
1352
|
-
path = line[3..].to_s.strip
|
|
1353
|
-
next if path.blank?
|
|
1354
|
-
|
|
1355
|
-
{ status: line[0..1].strip, path: path }
|
|
1356
|
-
end
|
|
1357
|
-
end
|
|
1358
|
-
|
|
1359
|
-
def parse_name_status(output)
|
|
1360
|
-
output.lines.filter_map do |line|
|
|
1361
|
-
parts = line.strip.split("\t")
|
|
1362
|
-
next if parts.empty?
|
|
1363
|
-
|
|
1364
|
-
status = parts[0].to_s.strip
|
|
1365
|
-
path = parts.last.to_s.strip
|
|
1366
|
-
next if path.blank?
|
|
1367
|
-
|
|
1368
|
-
{ status: status, path: path }
|
|
1369
|
-
end
|
|
1370
|
-
end
|
|
1371
|
-
|
|
1372
994
|
def monaco_worker_file
|
|
1373
995
|
engine_path = Mbeditor::Engine.root.join("public", "monaco_worker.js")
|
|
1374
996
|
return engine_path if engine_path.file?
|
|
@@ -1399,7 +1021,7 @@ module Mbeditor
|
|
|
1399
1021
|
|
|
1400
1022
|
def render_file_too_large(size)
|
|
1401
1023
|
render json: {
|
|
1402
|
-
error: "File is too large to open (#{human_size(size)}). Limit is #{human_size(
|
|
1024
|
+
error: "File is too large to open (#{human_size(size)}). Limit is #{human_size(FileOperationService::MAX_FILE_SIZE_BYTES)}."
|
|
1403
1025
|
}, status: :content_too_large
|
|
1404
1026
|
end
|
|
1405
1027
|
|