mbeditor 0.2.6 → 0.2.7
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/app/assets/javascripts/mbeditor/components/EditorPanel.js +1 -1
- data/app/assets/javascripts/mbeditor/editor_plugins.js +56 -29
- data/app/assets/javascripts/mbeditor/file_service.js +3 -2
- data/app/services/mbeditor/ruby_definition_service.rb +112 -33
- data/lib/mbeditor/engine.rb +5 -0
- data/lib/mbeditor/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 07df95cd9d266964f70a477f60b2bf31ae3f1b5f9fd2e24a407cf0ed8b677742
|
|
4
|
+
data.tar.gz: 2ddf51af23a7fc55466759f8349e0e56dd509777f29ef53653636b81157de562
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f61404e12d84ebea5e125ab3fd359d802160f8637de8c38256e571d471bfe823d060bb895bbcb495e803de4916bdf0a3e59c974e808fde67d1117a9905ecbea8
|
|
7
|
+
data.tar.gz: 705206cdada3581c56834307340ce9d184c2a299ed32fa788be97e5067d5fb3b5c0dea01f8fedba7ec1ea070b2c111b47210c1c800f8d7887886d8048eb5dc12
|
|
@@ -639,7 +639,7 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
639
639
|
if (onSaveRef.current) onSaveRef.current();
|
|
640
640
|
});
|
|
641
641
|
// Wire Ctrl+P to quick-open (VIM intercepts the key before the window listener sees it)
|
|
642
|
-
MonacoVim.VimMode.Vim.defineEx('mbeditorquickopen',
|
|
642
|
+
MonacoVim.VimMode.Vim.defineEx('mbeditorquickopen', null, function() {
|
|
643
643
|
EditorStore.setState({ isQuickOpenVisible: true });
|
|
644
644
|
});
|
|
645
645
|
MonacoVim.VimMode.Vim.map('<C-p>', ':mbeditorquickopen<CR>', 'normal');
|
|
@@ -695,6 +695,10 @@
|
|
|
695
695
|
// Ruby method definition hover provider.
|
|
696
696
|
// Calls the backend /definition endpoint (Ripper-based) and renders
|
|
697
697
|
// the method signature and any preceding # comments as hover markdown.
|
|
698
|
+
// Results are cached client-side for 60 s to make re-hovers instantaneous.
|
|
699
|
+
var hoverCache = {};
|
|
700
|
+
var HOVER_CACHE_TTL_MS = 60000;
|
|
701
|
+
|
|
698
702
|
monaco.languages.registerHoverProvider('ruby', {
|
|
699
703
|
provideHover: function provideHover(model, position, token) {
|
|
700
704
|
var wordInfo = model.getWordAtPosition(position);
|
|
@@ -707,51 +711,74 @@
|
|
|
707
711
|
|
|
708
712
|
var currentFile = model._mbeditorPath || null;
|
|
709
713
|
|
|
710
|
-
|
|
714
|
+
// Return cached result immediately if still fresh.
|
|
715
|
+
var cached = hoverCache[word];
|
|
716
|
+
if (cached && (Date.now() - cached.ts) < HOVER_CACHE_TTL_MS) {
|
|
717
|
+
var cachedResults = cached.results;
|
|
718
|
+
if (currentFile) {
|
|
719
|
+
cachedResults = cachedResults.filter(function(r) { return r.file !== currentFile; });
|
|
720
|
+
}
|
|
721
|
+
return cachedResults.length > 0 ? buildHoverResult(cachedResults) : null;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
// Cancel the underlying HTTP request when Monaco cancels the hover
|
|
725
|
+
// (e.g. user moved the mouse away before the response arrived).
|
|
726
|
+
var controller = typeof AbortController !== 'undefined' ? new AbortController() : null;
|
|
727
|
+
if (controller && token && token.onCancellationRequested) {
|
|
728
|
+
token.onCancellationRequested(function() { controller.abort(); });
|
|
729
|
+
}
|
|
730
|
+
var extraOptions = controller ? { signal: controller.signal } : {};
|
|
731
|
+
|
|
732
|
+
return FileService.getDefinition(word, 'ruby', extraOptions).then(function(data) {
|
|
711
733
|
// If the hover was cancelled while the request was in flight (e.g. the
|
|
712
734
|
// user moved the mouse away), return null so Monaco's CancelablePromise
|
|
713
735
|
// wrapper resolves cleanly instead of throwing "Canceled".
|
|
714
736
|
if (token && token.isCancellationRequested) return null;
|
|
715
737
|
|
|
716
738
|
var results = data && Array.isArray(data.results) ? data.results : [];
|
|
717
|
-
//
|
|
718
|
-
|
|
739
|
+
// Cache the raw results (before current-file filter).
|
|
740
|
+
hoverCache[word] = { ts: Date.now(), results: results };
|
|
741
|
+
|
|
719
742
|
if (currentFile) {
|
|
720
743
|
results = results.filter(function(r) { return r.file !== currentFile; });
|
|
721
744
|
}
|
|
722
745
|
if (results.length === 0) return null;
|
|
723
746
|
|
|
724
|
-
|
|
747
|
+
return buildHoverResult(results);
|
|
748
|
+
}).catch(function() { return null; });
|
|
749
|
+
}
|
|
750
|
+
});
|
|
751
|
+
|
|
752
|
+
function buildHoverResult(results) {
|
|
753
|
+
var first = results[0];
|
|
725
754
|
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
755
|
+
// Build two separate MarkdownString sections so Monaco renders a
|
|
756
|
+
// visual divider between the code block and the documentation.
|
|
757
|
+
var codeParts = ['```ruby'];
|
|
729
758
|
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
759
|
+
// Include a trimmed comment block as a Ruby comment inside the code
|
|
760
|
+
// fence so the whole thing looks like source you'd read in an editor.
|
|
761
|
+
if (first.comments && first.comments.length > 0) {
|
|
762
|
+
first.comments.split('\n').forEach(function(l) {
|
|
763
|
+
codeParts.push(l.trim() || '#');
|
|
764
|
+
});
|
|
765
|
+
}
|
|
737
766
|
|
|
738
|
-
|
|
739
|
-
|
|
767
|
+
codeParts.push(first.signature);
|
|
768
|
+
codeParts.push('```');
|
|
740
769
|
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
770
|
+
var fileRef = first.line > 0 ? first.file + ':' + first.line : first.file;
|
|
771
|
+
var locationParts = results.length > 1
|
|
772
|
+
? fileRef + ' _(+' + (results.length - 1) + ' more)_'
|
|
773
|
+
: fileRef;
|
|
745
774
|
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
}
|
|
754
|
-
});
|
|
775
|
+
return {
|
|
776
|
+
contents: [
|
|
777
|
+
{ value: codeParts.join('\n'), isTrusted: true },
|
|
778
|
+
{ value: '<span style="opacity:0.55;font-size:0.9em;">' + locationParts + '</span>', isTrusted: true, supportHtml: true }
|
|
779
|
+
]
|
|
780
|
+
};
|
|
781
|
+
}
|
|
755
782
|
}
|
|
756
783
|
|
|
757
784
|
window.MbeditorEditorPlugins = {
|
|
@@ -71,8 +71,9 @@ var FileService = (function () {
|
|
|
71
71
|
return axios.post(basePath() + '/state', { state: state }).then(function(res) { return res.data; });
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
function getDefinition(symbol, language) {
|
|
75
|
-
|
|
74
|
+
function getDefinition(symbol, language, extraOptions) {
|
|
75
|
+
var config = Object.assign({ params: { symbol: symbol, language: language }, timeout: 5000 }, extraOptions || {});
|
|
76
|
+
return axios.get(basePath() + '/definition', config).then(function(res) { return res.data; });
|
|
76
77
|
}
|
|
77
78
|
|
|
78
79
|
return {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "find"
|
|
4
|
+
require "json"
|
|
4
5
|
require "ripper"
|
|
5
6
|
|
|
6
7
|
module Mbeditor
|
|
@@ -22,10 +23,72 @@ module Mbeditor
|
|
|
22
23
|
MAX_RESULTS = 20
|
|
23
24
|
MAX_COMMENT_LOOKAHEAD = 15
|
|
24
25
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
# In-process file-index cache.
|
|
27
|
+
# Structure: { absolute_path => { mtime: Float, lines: [String], all_defs: { method_name => [line, ...] } } }
|
|
28
|
+
# Protected by a Mutex; entries are invalidated lazily via mtime comparison.
|
|
29
|
+
# When +cache_path+ is set (done automatically by the engine initializer), the
|
|
30
|
+
# cache is persisted to disk as JSON so it survives process restarts.
|
|
31
|
+
@file_cache = {}
|
|
32
|
+
@mutex = Mutex.new
|
|
33
|
+
@cache_path = nil
|
|
34
|
+
@cache_loaded = false
|
|
35
|
+
|
|
36
|
+
class << self
|
|
37
|
+
attr_reader :file_cache, :mutex
|
|
38
|
+
attr_accessor :cache_path
|
|
39
|
+
|
|
40
|
+
def call(workspace_root, symbol, excluded_dirnames: [], excluded_paths: [])
|
|
41
|
+
new(workspace_root, symbol,
|
|
42
|
+
excluded_dirnames: excluded_dirnames,
|
|
43
|
+
excluded_paths: excluded_paths).call
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Load the JSON cache from disk exactly once per process (double-checked
|
|
47
|
+
# under the mutex so concurrent first-calls don't double-load).
|
|
48
|
+
def load_disk_cache_once
|
|
49
|
+
return if @cache_loaded
|
|
50
|
+
|
|
51
|
+
@mutex.synchronize do
|
|
52
|
+
return if @cache_loaded
|
|
53
|
+
|
|
54
|
+
@cache_loaded = true
|
|
55
|
+
path = @cache_path.to_s
|
|
56
|
+
return if path.empty? || !File.exist?(path)
|
|
57
|
+
|
|
58
|
+
raw = JSON.parse(File.read(path))
|
|
59
|
+
raw.each do |abs_path, entry|
|
|
60
|
+
@file_cache[abs_path] = {
|
|
61
|
+
mtime: entry["mtime"].to_f,
|
|
62
|
+
lines: entry["lines"],
|
|
63
|
+
all_defs: entry["all_defs"]
|
|
64
|
+
}
|
|
65
|
+
end
|
|
66
|
+
rescue StandardError
|
|
67
|
+
nil # corrupted or incompatible cache file — start fresh
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Atomically write the in-memory cache to disk (tmp-file + rename).
|
|
72
|
+
def persist_cache
|
|
73
|
+
path = @cache_path.to_s
|
|
74
|
+
return if path.empty?
|
|
75
|
+
|
|
76
|
+
snapshot = @mutex.synchronize { @file_cache.dup }
|
|
77
|
+
tmp_path = "#{path}.tmp"
|
|
78
|
+
File.write(tmp_path, JSON.generate(snapshot))
|
|
79
|
+
File.rename(tmp_path, path)
|
|
80
|
+
rescue StandardError
|
|
81
|
+
nil
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Exposed for tests.
|
|
85
|
+
def clear_cache!
|
|
86
|
+
@mutex.synchronize { @file_cache.clear; @cache_loaded = false }
|
|
87
|
+
path = @cache_path.to_s
|
|
88
|
+
File.delete(path) if !path.empty? && File.exist?(path)
|
|
89
|
+
rescue StandardError
|
|
90
|
+
nil
|
|
91
|
+
end
|
|
29
92
|
end
|
|
30
93
|
|
|
31
94
|
def initialize(workspace_root, symbol, excluded_dirnames: [], excluded_paths: [])
|
|
@@ -36,7 +99,10 @@ module Mbeditor
|
|
|
36
99
|
end
|
|
37
100
|
|
|
38
101
|
def call
|
|
39
|
-
|
|
102
|
+
self.class.load_disk_cache_once
|
|
103
|
+
|
|
104
|
+
results = []
|
|
105
|
+
@new_entries = false
|
|
40
106
|
|
|
41
107
|
Find.find(@workspace_root) do |path|
|
|
42
108
|
# Prune excluded directories
|
|
@@ -55,16 +121,18 @@ module Mbeditor
|
|
|
55
121
|
next if excluded_rel_path?(rel, File.basename(path))
|
|
56
122
|
|
|
57
123
|
begin
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
hits = find_definitions(source, @symbol)
|
|
124
|
+
cached = cache_entry_for(path)
|
|
125
|
+
next unless cached
|
|
61
126
|
|
|
62
|
-
|
|
127
|
+
hit_lines = cached[:all_defs][@symbol]
|
|
128
|
+
next unless hit_lines && hit_lines.any?
|
|
129
|
+
|
|
130
|
+
hit_lines.each do |def_line|
|
|
63
131
|
results << {
|
|
64
132
|
file: rel,
|
|
65
133
|
line: def_line,
|
|
66
|
-
signature: (lines[def_line - 1] || "").strip,
|
|
67
|
-
comments: extract_comments(lines, def_line)
|
|
134
|
+
signature: (cached[:lines][def_line - 1] || "").strip,
|
|
135
|
+
comments: extract_comments(cached[:lines], def_line)
|
|
68
136
|
}
|
|
69
137
|
return results if results.length >= MAX_RESULTS
|
|
70
138
|
end
|
|
@@ -73,50 +141,61 @@ module Mbeditor
|
|
|
73
141
|
end
|
|
74
142
|
end
|
|
75
143
|
|
|
144
|
+
self.class.persist_cache if @new_entries
|
|
76
145
|
results
|
|
77
146
|
end
|
|
78
147
|
|
|
79
148
|
private
|
|
80
149
|
|
|
81
|
-
#
|
|
82
|
-
#
|
|
83
|
-
def
|
|
84
|
-
|
|
85
|
-
|
|
150
|
+
# Returns the cached index entry for +path+, rebuilding it if the file has
|
|
151
|
+
# been modified since the last parse. Returns nil on any read/parse error.
|
|
152
|
+
def cache_entry_for(path)
|
|
153
|
+
mtime = File.mtime(path).to_f
|
|
154
|
+
cached = self.class.mutex.synchronize { self.class.file_cache[path] }
|
|
155
|
+
return cached if cached && cached[:mtime] == mtime
|
|
156
|
+
|
|
157
|
+
source = File.read(path, encoding: "UTF-8", invalid: :replace, undef: :replace)
|
|
158
|
+
lines = source.split("\n", -1)
|
|
159
|
+
sexp = Ripper.sexp(source)
|
|
160
|
+
all_defs = sexp ? collect_all_defs(sexp) : {}
|
|
161
|
+
entry = { mtime: mtime, lines: lines, all_defs: all_defs }
|
|
162
|
+
self.class.mutex.synchronize { self.class.file_cache[path] = entry }
|
|
163
|
+
@new_entries = true
|
|
164
|
+
entry
|
|
165
|
+
rescue StandardError
|
|
166
|
+
nil
|
|
167
|
+
end
|
|
86
168
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
169
|
+
# Walk the Ripper sexp once and collect every method definition, returning
|
|
170
|
+
# a hash of the form { "method_name" => [line_number, ...] }.
|
|
171
|
+
def collect_all_defs(sexp)
|
|
172
|
+
defs = Hash.new { |h, k| h[k] = [] }
|
|
173
|
+
walk_all(sexp, defs)
|
|
174
|
+
defs
|
|
90
175
|
end
|
|
91
176
|
|
|
92
|
-
|
|
93
|
-
# def foo(...) as [:def, [:@ident, "foo", [line, col]], ...]
|
|
94
|
-
# def self.foo as [:defs, receiver, [:@ident, "foo", [line, col]], ...]
|
|
95
|
-
def walk(node, symbol, lines)
|
|
177
|
+
def walk_all(node, defs)
|
|
96
178
|
return unless node.is_a?(Array)
|
|
97
179
|
|
|
98
180
|
case node[0]
|
|
99
181
|
when :def
|
|
100
|
-
# node[1] is the method name node: [:@ident, "name", [line, col]]
|
|
101
182
|
name_node = node[1]
|
|
102
|
-
if name_node.is_a?(Array) && name_node[1].
|
|
183
|
+
if name_node.is_a?(Array) && name_node[1].is_a?(String)
|
|
103
184
|
line = name_node[2]&.first
|
|
104
|
-
|
|
185
|
+
defs[name_node[1]] << line if line
|
|
105
186
|
end
|
|
106
|
-
|
|
107
|
-
node[1..].each { |child| walk(child, symbol, lines) }
|
|
187
|
+
node[1..].each { |child| walk_all(child, defs) }
|
|
108
188
|
|
|
109
189
|
when :defs
|
|
110
|
-
# node[3] is the method name node for `def self.foo`
|
|
111
190
|
name_node = node[3]
|
|
112
|
-
if name_node.is_a?(Array) && name_node[1].
|
|
191
|
+
if name_node.is_a?(Array) && name_node[1].is_a?(String)
|
|
113
192
|
line = name_node[2]&.first
|
|
114
|
-
|
|
193
|
+
defs[name_node[1]] << line if line
|
|
115
194
|
end
|
|
116
|
-
node[1..].each { |child|
|
|
195
|
+
node[1..].each { |child| walk_all(child, defs) }
|
|
117
196
|
|
|
118
197
|
else
|
|
119
|
-
node.each { |child|
|
|
198
|
+
node.each { |child| walk_all(child, defs) }
|
|
120
199
|
end
|
|
121
200
|
end
|
|
122
201
|
|
data/lib/mbeditor/engine.rb
CHANGED
|
@@ -8,6 +8,11 @@ module Mbeditor
|
|
|
8
8
|
app.middleware.insert_before Rails::Rack::Logger, Mbeditor::Rack::SilencePingRequest
|
|
9
9
|
end
|
|
10
10
|
|
|
11
|
+
config.after_initialize do
|
|
12
|
+
Mbeditor::RubyDefinitionService.cache_path =
|
|
13
|
+
Rails.root.join("tmp", "mbeditor_ruby_defs.json").to_s
|
|
14
|
+
end
|
|
15
|
+
|
|
11
16
|
initializer "mbeditor.assets.precompile" do |app|
|
|
12
17
|
app.config.assets.precompile += %w[
|
|
13
18
|
mbeditor/application.css
|
data/lib/mbeditor/version.rb
CHANGED