mbeditor 0.12.5 → 0.12.6
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 +69 -0
- data/app/assets/javascripts/mbeditor/components/EditorPanel.js +8 -2
- data/app/assets/javascripts/mbeditor/components/MbeditorApp.js +186 -179
- data/app/assets/javascripts/mbeditor/editor_plugins.js +138 -0
- data/app/assets/javascripts/mbeditor/file_service.js +14 -0
- data/app/controllers/mbeditor/application_controller.rb +10 -5
- data/app/services/mbeditor/code_search_service.rb +1 -1
- data/app/services/mbeditor/git_service.rb +6 -3
- data/app/services/mbeditor/search_replace_service.rb +14 -1
- data/lib/mbeditor/rack/silence_ping_request.rb +9 -1
- data/lib/mbeditor/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: dffea86be286ee0bdcb216589b12264dcac613a33e59aa2828490706785e1825
|
|
4
|
+
data.tar.gz: f786fe449deccc7ff1971c8df15015b7ee0507b45b14a332f83e162b44facc01
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b1cf4fcf24d9868b1b5a284c1de2f8eb3b5032a10d8cb3a9ebb1677eed3a6ce9660e658a43f80652ec1a3edb5195aadcf01fc428fc381ed5f62618d59dde4286
|
|
7
|
+
data.tar.gz: 3c0c8dc6d5d2aee702d0ed1af2814e884cd204702383dafc21871ffcf2a0b8fa8e871a971d69824ea44f7831f335d3f7e8d0999c832142e5b510d43033ad10cd
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,75 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.12.6] - 2026-08-05
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **Format All.** A second toolbar button formats every open document. Each tab
|
|
14
|
+
is formatted independently and failures are collected rather than thrown, so
|
|
15
|
+
one unparseable file cannot stop the rest; the status line reports how many
|
|
16
|
+
were formatted, skipped and failed.
|
|
17
|
+
- **Format on paste actually formats.** `formatOnPaste` has been on by default
|
|
18
|
+
for a long time and did nothing outside Ruby: Monaco acts on a paste only
|
|
19
|
+
through a *range* formatting provider, and none was registered. Prettier-backed
|
|
20
|
+
range and document providers now cover JS/JSX, JSON, CSS/SCSS/LESS, HTML and
|
|
21
|
+
Markdown. A paste that fills the document is formatted as a document — so
|
|
22
|
+
pasting into a blank file formats the whole thing — while a paste in the middle
|
|
23
|
+
is formatted as a range, reprinting the smallest enclosing statement and
|
|
24
|
+
leaving the rest of the file byte-identical. Code copied in from a spaces
|
|
25
|
+
project therefore lands as tabs (or the reverse), which was previously a manual
|
|
26
|
+
chore that never converted cleanly.
|
|
27
|
+
- **Files symlinked in from outside the workspace now open.** An app that links a
|
|
28
|
+
shared config directory or a sibling engine into its tree could see those files
|
|
29
|
+
in the explorer but not open them — the tab opened and closed itself a moment
|
|
30
|
+
later. Symlinks are now followed the way a file manager does. Containment is
|
|
31
|
+
still enforced: `File.expand_path` collapses `..` before a lexical check
|
|
32
|
+
against the workspace root, so no request can name anything outside it, and
|
|
33
|
+
git operations stay strictly inside the repo.
|
|
34
|
+
|
|
35
|
+
### Fixed
|
|
36
|
+
- **Search returned nothing, instantly, on some workspaces without ripgrep.** The
|
|
37
|
+
git-grep tier was chosen on the mere presence of a `.git` entry, with no check
|
|
38
|
+
that git could actually open the repo. When it cannot — dubious ownership under
|
|
39
|
+
Docker, a `.git` file pointing at a gitdir that moved, no git binary on the
|
|
40
|
+
server's `PATH` — `git grep` fails, its stderr goes to `/dev/null`, and the
|
|
41
|
+
result is an empty set indistinguishable from "no matches". The tier is now
|
|
42
|
+
gated on `git rev-parse --is-inside-work-tree`, which fails in exactly those
|
|
43
|
+
cases, so a bad repo falls through to plain grep. Definition lookups had the
|
|
44
|
+
same check and were silently broken the same way.
|
|
45
|
+
- **Formatting ignored the tab/space setting.** There were four copies of the
|
|
46
|
+
Prettier options and they had drifted: two read a `prettierTabWidth` /
|
|
47
|
+
`prettierUseTabs` pair that no settings screen ever wrote, frozen at two
|
|
48
|
+
spaces, overriding the `tabSize` / `insertSpaces` actually configured. A JSX
|
|
49
|
+
file in a tabs workspace came back space-indented. Indentation now comes from
|
|
50
|
+
the same preferences Monaco itself is given.
|
|
51
|
+
- **One JS file had two formatters that disagreed.** Monaco's TypeScript worker
|
|
52
|
+
registers its own range formatter for `javascript` and Monaco uses whichever it
|
|
53
|
+
finds first, so the toolbar button went through Prettier while `Shift+Alt+F`
|
|
54
|
+
and format-on-paste got the worker's two-space, no-semicolon output. The
|
|
55
|
+
worker's formatting is switched off; its completions, hovers, diagnostics and
|
|
56
|
+
rename are untouched.
|
|
57
|
+
- **"Server offline" flashed during ordinary actions.** The reachability check
|
|
58
|
+
counted any error without a response as a network failure, and a request the
|
|
59
|
+
editor aborts itself has none either. The editor cancels constantly — each
|
|
60
|
+
keystroke supersedes the previous search, hover and completion providers abort
|
|
61
|
+
when the cursor moves on, opening a file aborts its own prefetch — so two in a
|
|
62
|
+
row declared the server offline until the `/ping` probe a second later put it
|
|
63
|
+
back. Creating a file reliably triggered it. Cancellations are now ignored; a
|
|
64
|
+
genuinely unreachable host still marks offline and still recovers.
|
|
65
|
+
- **Warnings logged by the host app from mbeditor's own hooks were swallowed.**
|
|
66
|
+
Editor requests were silenced at `ERROR`, which hides Rails' own INFO-level
|
|
67
|
+
request lines but also everything a host app logs from inside
|
|
68
|
+
`authenticate_with` or `user_name_callback` — those procs run within the
|
|
69
|
+
request, so a developer debugging their own hook saw nothing and had no way to
|
|
70
|
+
know why. Silenced at `WARN` now, which still hides the request lines.
|
|
71
|
+
|
|
72
|
+
### Changed
|
|
73
|
+
- RuboCop's output is taken as-is. Ruby indentation belongs to the project's
|
|
74
|
+
`.rubocop.yml`, so rewriting it in the editor fought the linter about to run
|
|
75
|
+
over the same file. The previous code converted the source to tabs *before*
|
|
76
|
+
handing it to RuboCop, which discarded it. To convert an already-open file,
|
|
77
|
+
use Monaco's built-in "Convert Indentation to Tabs / to Spaces" (F1).
|
|
78
|
+
|
|
10
79
|
## [0.12.5] - 2026-08-03
|
|
11
80
|
|
|
12
81
|
### Fixed
|
|
@@ -643,7 +643,10 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
643
643
|
autoClosingBrackets: editorPrefs.autoClosingBrackets || 'always',
|
|
644
644
|
autoClosingQuotes: editorPrefs.autoClosingQuotes || 'always',
|
|
645
645
|
autoIndent: editorPrefs.autoIndent || 'full',
|
|
646
|
-
|
|
646
|
+
// Monaco's own format-on-paste is left off: it never ran the formatter
|
|
647
|
+
// here. attachEditorFeatures drives it from onDidPaste instead, gated on
|
|
648
|
+
// the same editorPrefs.formatOnPaste setting.
|
|
649
|
+
formatOnPaste: false,
|
|
647
650
|
formatOnType: editorPrefs.formatOnType === true, // off by default: on-type formatting adds per-keystroke latency on slow machines
|
|
648
651
|
quickSuggestions: editorPrefs.quickSuggestions !== false,
|
|
649
652
|
wordBasedSuggestions: editorPrefs.wordBasedSuggestions || 'currentDocument',
|
|
@@ -1206,7 +1209,10 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
1206
1209
|
autoClosingBrackets: editorPrefs.autoClosingBrackets || 'always',
|
|
1207
1210
|
autoClosingQuotes: editorPrefs.autoClosingQuotes || 'always',
|
|
1208
1211
|
autoIndent: editorPrefs.autoIndent || 'full',
|
|
1209
|
-
|
|
1212
|
+
// Monaco's own format-on-paste is left off: it never ran the formatter
|
|
1213
|
+
// here. attachEditorFeatures drives it from onDidPaste instead, gated on
|
|
1214
|
+
// the same editorPrefs.formatOnPaste setting.
|
|
1215
|
+
formatOnPaste: false,
|
|
1210
1216
|
formatOnType: editorPrefs.formatOnType === true, // off by default: on-type formatting adds per-keystroke latency on slow machines
|
|
1211
1217
|
quickSuggestions: editorPrefs.quickSuggestions !== false,
|
|
1212
1218
|
wordBasedSuggestions: editorPrefs.wordBasedSuggestions || 'currentDocument',
|
|
@@ -39,7 +39,52 @@ var GIT_PANEL_MIN_WIDTH = 280;
|
|
|
39
39
|
var PANE_MIN_WIDTH_PERCENT = 20;
|
|
40
40
|
var PANE_MAX_WIDTH_PERCENT = 80;
|
|
41
41
|
var SIDEBAR_COLLAPSED_WIDTH = 48;
|
|
42
|
-
|
|
42
|
+
// Extension -> Prettier parser. Limited to what the vendored plugins actually
|
|
43
|
+
// parse (babel, estree, html, postcss, markdown); anything outside this map
|
|
44
|
+
// falls through to Monaco's re-indent.
|
|
45
|
+
//
|
|
46
|
+
// One map, one options builder, one runner — there were four copies of each,
|
|
47
|
+
// and they had drifted: two read a `prettierTabWidth`/`prettierUseTabs` pair
|
|
48
|
+
// that no settings screen ever wrote, so Prettier reprinted every JS/JSX file
|
|
49
|
+
// at two spaces however the editor was configured.
|
|
50
|
+
var PRETTIER_PARSERS = {
|
|
51
|
+
js: 'babel', jsx: 'babel', mjs: 'babel', cjs: 'babel',
|
|
52
|
+
json: 'json', jsonc: 'json', json5: 'json5',
|
|
53
|
+
css: 'css', scss: 'scss', less: 'less',
|
|
54
|
+
html: 'html', vue: 'vue',
|
|
55
|
+
md: 'markdown', markdown: 'markdown'
|
|
56
|
+
};
|
|
57
|
+
var SUPPORTED_PRETTIER_EXTS = Object.keys(PRETTIER_PARSERS);
|
|
58
|
+
|
|
59
|
+
function prettierParserFor(path) {
|
|
60
|
+
return PRETTIER_PARSERS[String(path || '').split('.').pop().toLowerCase()] || null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Indentation comes from the editor's own tabSize/insertSpaces, so formatting
|
|
64
|
+
// produces what the user set up. `insertSpaces !== true` mirrors how
|
|
65
|
+
// EditorPanel configures Monaco: anything but an explicit true means tabs.
|
|
66
|
+
function prettierOptions(prefs, parserName, extra) {
|
|
67
|
+
return Object.assign({
|
|
68
|
+
parser: parserName,
|
|
69
|
+
plugins: Object.values(window.prettierPlugins || {}),
|
|
70
|
+
printWidth: prefs.prettierPrintWidth != null ? prefs.prettierPrintWidth : 80,
|
|
71
|
+
tabWidth: prefs.tabSize != null ? prefs.tabSize : 4,
|
|
72
|
+
useTabs: prefs.insertSpaces !== true,
|
|
73
|
+
semi: prefs.prettierSemi !== false,
|
|
74
|
+
singleQuote: !!prefs.prettierSingleQuote,
|
|
75
|
+
trailingComma: prefs.prettierTrailingComma || 'all',
|
|
76
|
+
bracketSpacing: prefs.prettierBracketSpacing !== false
|
|
77
|
+
}, extra || {});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Format with Prettier, loading it on first use. Rejects if it cannot be
|
|
81
|
+
// loaded, so callers can report that rather than appear to do nothing.
|
|
82
|
+
function runPrettier(source, prefs, parserName, extra) {
|
|
83
|
+
var go = function () { return window.prettier.format(source, prettierOptions(prefs, parserName, extra)); };
|
|
84
|
+
if (window.prettier && window.prettierPlugins) return go();
|
|
85
|
+
if (window.loadPrettierPlugins) return window.loadPrettierPlugins().then(go);
|
|
86
|
+
return Promise.reject(new Error('Prettier is not available'));
|
|
87
|
+
}
|
|
43
88
|
|
|
44
89
|
var DEFAULT_EDITOR_PREFS = {
|
|
45
90
|
theme: 'vs-dark',
|
|
@@ -74,8 +119,6 @@ var DEFAULT_EDITOR_PREFS = {
|
|
|
74
119
|
toolbarIconOnly: false,
|
|
75
120
|
rubocopLintEnabled: true,
|
|
76
121
|
prettierPrintWidth: 80,
|
|
77
|
-
prettierTabWidth: 2,
|
|
78
|
-
prettierUseTabs: false,
|
|
79
122
|
prettierSemi: true,
|
|
80
123
|
prettierSingleQuote: false,
|
|
81
124
|
prettierTrailingComma: 'all',
|
|
@@ -89,28 +132,11 @@ var DEFAULT_EDITOR_PREFS = {
|
|
|
89
132
|
branchStateRestore: true
|
|
90
133
|
};
|
|
91
134
|
|
|
92
|
-
//
|
|
93
|
-
//
|
|
94
|
-
//
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
code.split('\n').forEach(function(line) {
|
|
98
|
-
if (!line.trim()) return;
|
|
99
|
-
var m = line.match(/^( +)/);
|
|
100
|
-
if (m) min = Math.min(min, m[1].length);
|
|
101
|
-
});
|
|
102
|
-
return min === Infinity ? 0 : min;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// Convert leading space-based indentation to tabs using the detected unit size.
|
|
106
|
-
function spacesToTabs(code, indentSize) {
|
|
107
|
-
var unit = ' '.repeat(indentSize);
|
|
108
|
-
return code.split('\n').map(function(line) {
|
|
109
|
-
var tabs = '';
|
|
110
|
-
while (line.startsWith(unit)) { tabs += '\t'; line = line.slice(unit.length); }
|
|
111
|
-
return tabs + line;
|
|
112
|
-
}).join('\n');
|
|
113
|
-
}
|
|
135
|
+
// Indentation of formatted output is the formatter's job, not a post-pass:
|
|
136
|
+
// Prettier is given useTabs/tabWidth from the editor's own settings, and Ruby
|
|
137
|
+
// indentation comes from the project's .rubocop.yml. Monaco's built-in
|
|
138
|
+
// "Convert Indentation to Tabs / to Spaces" commands (F1) cover converting a
|
|
139
|
+
// file that is already open, using its own indentation guesser.
|
|
114
140
|
|
|
115
141
|
function diffLines(oldLines, newLines) {
|
|
116
142
|
var n = oldLines.length, m = newLines.length;
|
|
@@ -1018,28 +1044,11 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
1018
1044
|
return;
|
|
1019
1045
|
}
|
|
1020
1046
|
|
|
1021
|
-
var
|
|
1022
|
-
var formatMap = {
|
|
1023
|
-
'js': 'babel', 'jsx': 'babel',
|
|
1024
|
-
'json': 'json',
|
|
1025
|
-
'css': 'css', 'scss': 'scss',
|
|
1026
|
-
'html': 'html', 'md': 'markdown'
|
|
1027
|
-
};
|
|
1028
|
-
var parserName = formatMap[ext];
|
|
1047
|
+
var parserName = prettierParserFor(tab.path);
|
|
1029
1048
|
|
|
1030
1049
|
if (parserName && window.prettier && window.prettierPlugins) {
|
|
1031
1050
|
var prefs = EditorStore.getState().editorPrefs || DEFAULT_EDITOR_PREFS;
|
|
1032
|
-
window.prettier.format(tab.content, {
|
|
1033
|
-
parser: parserName,
|
|
1034
|
-
plugins: Object.values(window.prettierPlugins),
|
|
1035
|
-
printWidth: prefs.prettierPrintWidth != null ? prefs.prettierPrintWidth : 80,
|
|
1036
|
-
tabWidth: prefs.tabSize != null ? prefs.tabSize : 2,
|
|
1037
|
-
useTabs: !(prefs.insertSpaces),
|
|
1038
|
-
semi: prefs.prettierSemi !== false,
|
|
1039
|
-
singleQuote: !!prefs.prettierSingleQuote,
|
|
1040
|
-
trailingComma: prefs.prettierTrailingComma || 'all',
|
|
1041
|
-
bracketSpacing: prefs.prettierBracketSpacing !== false
|
|
1042
|
-
}).then(function () {
|
|
1051
|
+
window.prettier.format(tab.content, prettierOptions(prefs, parserName)).then(function () {
|
|
1043
1052
|
var currentPane = EditorStore.getState().panes.find(function (p) {
|
|
1044
1053
|
return p.id === paneId;
|
|
1045
1054
|
});
|
|
@@ -2671,26 +2680,9 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
2671
2680
|
.then(function (res) { return (res && res.content) || null; })
|
|
2672
2681
|
["catch"](function () { return null; });
|
|
2673
2682
|
}
|
|
2674
|
-
var
|
|
2675
|
-
var parserMap = { 'js': 'babel', 'jsx': 'babel', 'json': 'json', 'css': 'css', 'scss': 'scss', 'html': 'html', 'md': 'markdown' };
|
|
2676
|
-
var parserName = parserMap[ext];
|
|
2683
|
+
var parserName = prettierParserFor(tab.path);
|
|
2677
2684
|
if (!parserName) return Promise.resolve(null);
|
|
2678
|
-
|
|
2679
|
-
return window.prettier.format(tab.content, {
|
|
2680
|
-
parser: parserName,
|
|
2681
|
-
plugins: Object.values(window.prettierPlugins),
|
|
2682
|
-
printWidth: editorPrefs.prettierPrintWidth != null ? editorPrefs.prettierPrintWidth : 80,
|
|
2683
|
-
tabWidth: editorPrefs.prettierTabWidth != null ? editorPrefs.prettierTabWidth : 2,
|
|
2684
|
-
useTabs: !!editorPrefs.prettierUseTabs,
|
|
2685
|
-
semi: editorPrefs.prettierSemi !== false,
|
|
2686
|
-
singleQuote: !!editorPrefs.prettierSingleQuote,
|
|
2687
|
-
trailingComma: editorPrefs.prettierTrailingComma || 'all',
|
|
2688
|
-
bracketSpacing: editorPrefs.prettierBracketSpacing !== false
|
|
2689
|
-
})["catch"](function () { return null; });
|
|
2690
|
-
};
|
|
2691
|
-
if (window.prettier && window.prettierPlugins) return run();
|
|
2692
|
-
if (window.loadPrettierPlugins) return window.loadPrettierPlugins().then(run)["catch"](function () { return null; });
|
|
2693
|
-
return Promise.resolve(null);
|
|
2685
|
+
return runPrettier(tab.content, editorPrefs, parserName)["catch"](function () { return null; });
|
|
2694
2686
|
};
|
|
2695
2687
|
|
|
2696
2688
|
var handleSave = function handleSave(paneId, tab) {
|
|
@@ -2991,132 +2983,141 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
2991
2983
|
});
|
|
2992
2984
|
};
|
|
2993
2985
|
|
|
2994
|
-
|
|
2995
|
-
|
|
2986
|
+
// Format one tab's content.
|
|
2987
|
+
//
|
|
2988
|
+
// Resolves to the formatted string, or to null when no formatter covers this
|
|
2989
|
+
// file type (the caller then decides whether to fall back to a re-indent).
|
|
2990
|
+
// Rejects when a formatter ran and failed, so the reason reaches the user
|
|
2991
|
+
// rather than the button appearing to do nothing.
|
|
2992
|
+
var formatTabContent = function formatTabContent(tab, prefs) {
|
|
2993
|
+
prefs = prefs || editorPrefs;
|
|
2994
|
+
if (isRubyPath(tab.path) || tab.path.endsWith('.rake')) {
|
|
2995
|
+
if (!rubocopAvailable) return Promise.reject(new Error("RuboCop is not available for this workspace."));
|
|
2996
|
+
// RuboCop's output is taken as-is. Ruby indentation belongs to the
|
|
2997
|
+
// project's .rubocop.yml (Layout/IndentationStyle and friends), so
|
|
2998
|
+
// rewriting it here would fight the linter that is about to be run over
|
|
2999
|
+
// the same file. The old code tried the opposite — converting the source
|
|
3000
|
+
// to tabs *before* handing it over — which RuboCop simply discarded.
|
|
3001
|
+
return formatRubySource(tab.path, tab.content).then(function (res) {
|
|
3002
|
+
return (res && res.content) || null;
|
|
3003
|
+
});
|
|
3004
|
+
}
|
|
2996
3005
|
|
|
2997
|
-
var
|
|
3006
|
+
var parserName = prettierParserFor(tab.path);
|
|
3007
|
+
if (!parserName) return Promise.resolve(null);
|
|
3008
|
+
return runPrettier(tab.content, prefs, parserName);
|
|
3009
|
+
};
|
|
2998
3010
|
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3011
|
+
// Write formatted content back to a tab, dirty and unsaved — the user decides
|
|
3012
|
+
// when to save. EditorPanel applies it through executeEdits, so undo works.
|
|
3013
|
+
var applyFormattedContent = function applyFormattedContent(paneId, tabId, formatted) {
|
|
3014
|
+
EditorStore.setState({
|
|
3015
|
+
panes: EditorStore.getState().panes.map(function (p) {
|
|
3016
|
+
if (p.id !== paneId) return p;
|
|
3017
|
+
return _extends({}, p, { tabs: p.tabs.map(function (t) {
|
|
3018
|
+
return t.id === tabId
|
|
3019
|
+
? _extends({}, t, { content: formatted, dirty: true, externalContentVersion: (t.externalContentVersion || 0) + 1 })
|
|
3020
|
+
: t;
|
|
3021
|
+
}) });
|
|
3022
|
+
})
|
|
3023
|
+
});
|
|
3024
|
+
};
|
|
3003
3025
|
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
return { range: new monaco.Range(ln, 1, ln, 1), options: { isWholeLine: true, className: 'mbeditor-format-changed' } };
|
|
3034
|
-
});
|
|
3035
|
-
var ids = monacoEditor.deltaDecorations([], decorations);
|
|
3036
|
-
setTimeout(function() { monacoEditor.deltaDecorations(ids, []); }, 3000);
|
|
3037
|
-
}
|
|
3038
|
-
}
|
|
3026
|
+
var highlightFormatChanges = function highlightFormatChanges(before, after) {
|
|
3027
|
+
var monacoEditor = window.__mbeditorActiveEditor;
|
|
3028
|
+
if (!monacoEditor || before === after) return;
|
|
3029
|
+
var changedLineNums = diffLines(before.split('\n'), after.split('\n'));
|
|
3030
|
+
if (!changedLineNums.length) return;
|
|
3031
|
+
var ids = monacoEditor.deltaDecorations([], changedLineNums.map(function (ln) {
|
|
3032
|
+
return { range: new monaco.Range(ln, 1, ln, 1), options: { isWholeLine: true, className: 'mbeditor-format-changed' } };
|
|
3033
|
+
}));
|
|
3034
|
+
setTimeout(function () { monacoEditor.deltaDecorations(ids, []); }, 3000);
|
|
3035
|
+
};
|
|
3036
|
+
|
|
3037
|
+
var handleFormat = function handleFormat() {
|
|
3038
|
+
if (!activeTab) return;
|
|
3039
|
+
|
|
3040
|
+
var tab = activeTab;
|
|
3041
|
+
var paneId = focusedPane.id;
|
|
3042
|
+
var originalContent = tab.content;
|
|
3043
|
+
|
|
3044
|
+
setLoading(function (prev) { return _extends({}, prev, { format: true }); });
|
|
3045
|
+
EditorStore.setStatus("Formatting " + tab.name + "...", "info");
|
|
3046
|
+
|
|
3047
|
+
formatTabContent(tab).then(function (formatted) {
|
|
3048
|
+
if (formatted == null) {
|
|
3049
|
+
// No formatter for this file type — re-indent with Monaco instead.
|
|
3050
|
+
var monacoEditor = window.__mbeditorActiveEditor;
|
|
3051
|
+
var reindentAction = monacoEditor && monacoEditor.getAction('editor.action.reindentLines');
|
|
3052
|
+
if (!reindentAction) {
|
|
3053
|
+
EditorStore.setStatus("No formatter for " + tab.name + ".", "warning");
|
|
3054
|
+
return;
|
|
3039
3055
|
}
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
})["catch"](function (err) {
|
|
3043
|
-
return EditorStore.setStatus("Format failed: " + err.message, "error");
|
|
3044
|
-
})["finally"](function () {
|
|
3045
|
-
return setLoading(function (prev) {
|
|
3046
|
-
return _extends({}, prev, { format: false });
|
|
3056
|
+
return reindentAction.run().then(function () {
|
|
3057
|
+
EditorStore.setStatus("Re-indented (Unsaved)", "success");
|
|
3047
3058
|
});
|
|
3059
|
+
}
|
|
3060
|
+
if (formatted !== originalContent) {
|
|
3061
|
+
applyFormattedContent(paneId, tab.id, formatted);
|
|
3062
|
+
highlightFormatChanges(originalContent, formatted);
|
|
3063
|
+
}
|
|
3064
|
+
EditorStore.setStatus(formatted === originalContent ? "Already formatted" : "Formatted (Unsaved)", "success");
|
|
3065
|
+
GitService.fetchStatus();
|
|
3066
|
+
})["catch"](function (err) {
|
|
3067
|
+
EditorStore.setStatus("Format failed: " + (err && err.message ? err.message : err), "error");
|
|
3068
|
+
})["finally"](function () {
|
|
3069
|
+
setLoading(function (prev) { return _extends({}, prev, { format: false }); });
|
|
3070
|
+
});
|
|
3071
|
+
};
|
|
3072
|
+
|
|
3073
|
+
// Format every open document across all panes.
|
|
3074
|
+
//
|
|
3075
|
+
// Each tab is formatted independently and a failure is collected rather than
|
|
3076
|
+
// thrown, so one unparseable file cannot stop the rest. Virtual tabs (diffs,
|
|
3077
|
+
// settings, the changelog) have no path on disk and are skipped.
|
|
3078
|
+
var handleFormatAll = function handleFormatAll() {
|
|
3079
|
+
var targets = [];
|
|
3080
|
+
EditorStore.getState().panes.forEach(function (p) {
|
|
3081
|
+
p.tabs.forEach(function (t) {
|
|
3082
|
+
if (t.path && !t.path.startsWith('mbeditor://') && t.path !== '__settings__' && typeof t.content === 'string') {
|
|
3083
|
+
targets.push({ paneId: p.id, tab: t });
|
|
3084
|
+
}
|
|
3048
3085
|
});
|
|
3086
|
+
});
|
|
3087
|
+
|
|
3088
|
+
if (!targets.length) {
|
|
3089
|
+
EditorStore.setStatus("No open documents to format.", "warning");
|
|
3049
3090
|
return;
|
|
3050
3091
|
}
|
|
3051
3092
|
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
var formatMap = {
|
|
3055
|
-
'js': 'babel', 'jsx': 'babel',
|
|
3056
|
-
'json': 'json',
|
|
3057
|
-
'css': 'css', 'scss': 'scss',
|
|
3058
|
-
'html': 'html', 'md': 'markdown'
|
|
3059
|
-
};
|
|
3060
|
-
var parserName = formatMap[ext];
|
|
3093
|
+
setLoading(function (prev) { return _extends({}, prev, { format: true }); });
|
|
3094
|
+
EditorStore.setStatus("Formatting " + targets.length + " open document" + (targets.length === 1 ? "" : "s") + "...", "info");
|
|
3061
3095
|
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
});
|
|
3066
|
-
EditorStore.setStatus("Formatting with Prettier...", "info");
|
|
3067
|
-
var doFormat = function doFormat() {
|
|
3068
|
-
return window.prettier.format(activeTab.content, {
|
|
3069
|
-
parser: parserName,
|
|
3070
|
-
plugins: Object.values(window.prettierPlugins),
|
|
3071
|
-
printWidth: editorPrefs.prettierPrintWidth != null ? editorPrefs.prettierPrintWidth : 80,
|
|
3072
|
-
tabWidth: editorPrefs.prettierTabWidth != null ? editorPrefs.prettierTabWidth : 2,
|
|
3073
|
-
useTabs: !!editorPrefs.prettierUseTabs,
|
|
3074
|
-
semi: editorPrefs.prettierSemi !== false,
|
|
3075
|
-
singleQuote: !!editorPrefs.prettierSingleQuote,
|
|
3076
|
-
trailingComma: editorPrefs.prettierTrailingComma || 'all',
|
|
3077
|
-
bracketSpacing: editorPrefs.prettierBracketSpacing !== false
|
|
3078
|
-
}).then(function (formatted) {
|
|
3079
|
-
var newPanes = EditorStore.getState().panes.map(function (p) {
|
|
3080
|
-
if (p.id === focusedPane.id) return _extends({}, p, { tabs: p.tabs.map(function (t) {
|
|
3081
|
-
return t.id === activeTab.id ? _extends({}, t, { content: formatted, dirty: true, externalContentVersion: (t.externalContentVersion || 0) + 1 }) : t;
|
|
3082
|
-
}) });
|
|
3083
|
-
return p;
|
|
3084
|
-
});
|
|
3085
|
-
EditorStore.setState({ panes: newPanes });
|
|
3086
|
-
EditorStore.setStatus("Formatted (Unsaved)", "success");
|
|
3087
|
-
GitService.fetchStatus();
|
|
3088
|
-
})["catch"](function (err) {
|
|
3089
|
-
EditorStore.setStatus("Prettier Formatter failed: " + err.message, "error");
|
|
3090
|
-
})["finally"](function () {
|
|
3091
|
-
setLoading(function (prev) {
|
|
3092
|
-
return _extends({}, prev, { format: false });
|
|
3093
|
-
});
|
|
3094
|
-
});
|
|
3095
|
-
};
|
|
3096
|
-
if (window.prettier && window.prettierPlugins) {
|
|
3097
|
-
doFormat();
|
|
3098
|
-
} else if (window.loadPrettierPlugins) {
|
|
3099
|
-
window.loadPrettierPlugins().then(doFormat)["catch"](function (err) {
|
|
3100
|
-
EditorStore.setStatus("Failed to load Prettier: " + err.message, "error");
|
|
3101
|
-
setLoading(function (prev) { return _extends({}, prev, { format: false }); });
|
|
3102
|
-
});
|
|
3103
|
-
} else {
|
|
3104
|
-
EditorStore.setStatus("Prettier is not available.", "warning");
|
|
3105
|
-
setLoading(function (prev) { return _extends({}, prev, { format: false }); });
|
|
3106
|
-
}
|
|
3107
|
-
return;
|
|
3108
|
-
}
|
|
3096
|
+
var changed = 0;
|
|
3097
|
+
var skipped = 0;
|
|
3098
|
+
var failures = [];
|
|
3109
3099
|
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
}
|
|
3119
|
-
}
|
|
3100
|
+
Promise.all(targets.map(function (target) {
|
|
3101
|
+
return formatTabContent(target.tab).then(function (formatted) {
|
|
3102
|
+
if (formatted == null) { skipped += 1; return; }
|
|
3103
|
+
if (formatted === target.tab.content) return;
|
|
3104
|
+
applyFormattedContent(target.paneId, target.tab.id, formatted);
|
|
3105
|
+
changed += 1;
|
|
3106
|
+
})["catch"](function (err) {
|
|
3107
|
+
failures.push(target.tab.name + ": " + (err && err.message ? err.message.split('\n')[0] : err));
|
|
3108
|
+
});
|
|
3109
|
+
})).then(function () {
|
|
3110
|
+
var parts = [changed + " formatted"];
|
|
3111
|
+
if (skipped) parts.push(skipped + " skipped");
|
|
3112
|
+
if (failures.length) parts.push(failures.length + " failed");
|
|
3113
|
+
EditorStore.setStatus(
|
|
3114
|
+
parts.join(", ") + (changed ? " (Unsaved)" : "") + (failures.length ? " — " + failures[0] : ""),
|
|
3115
|
+
failures.length ? "error" : "success"
|
|
3116
|
+
);
|
|
3117
|
+
if (changed) GitService.fetchStatus();
|
|
3118
|
+
})["finally"](function () {
|
|
3119
|
+
setLoading(function (prev) { return _extends({}, prev, { format: false }); });
|
|
3120
|
+
});
|
|
3120
3121
|
};
|
|
3121
3122
|
|
|
3122
3123
|
var TEST_CACHE_PREFIX = 'mbeditor_test_result_';
|
|
@@ -4226,10 +4227,16 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
4226
4227
|
React.createElement("div", { className: "statusbar-sep" }),
|
|
4227
4228
|
React.createElement(
|
|
4228
4229
|
"button",
|
|
4229
|
-
{ className: "statusbar-btn", onClick: handleFormat, disabled: loading.format || !canLintAndFormat, 'aria-busy': !!loading.format },
|
|
4230
|
+
{ className: "statusbar-btn", onClick: handleFormat, disabled: loading.format || !canLintAndFormat, 'aria-busy': !!loading.format, title: "Format this document" },
|
|
4230
4231
|
!loading.format && React.createElement("i", { className: "fas fa-magic" }),
|
|
4231
4232
|
!toolbarIconOnly && !loading.format && " Format"
|
|
4232
4233
|
),
|
|
4234
|
+
React.createElement(
|
|
4235
|
+
"button",
|
|
4236
|
+
{ className: "statusbar-btn", onClick: handleFormatAll, disabled: loading.format || !canLintAndFormat, 'aria-busy': !!loading.format, title: "Format all open documents" },
|
|
4237
|
+
React.createElement("i", { className: "fas fa-wand-magic-sparkles" }),
|
|
4238
|
+
!toolbarIconOnly && !loading.format && " Format All"
|
|
4239
|
+
),
|
|
4233
4240
|
hasGitBranch && React.createElement(
|
|
4234
4241
|
React.Fragment,
|
|
4235
4242
|
null,
|
|
@@ -816,12 +816,58 @@
|
|
|
816
816
|
return handled;
|
|
817
817
|
});
|
|
818
818
|
|
|
819
|
+
// ── Format on paste ──────────────────────────────────────────────────────
|
|
820
|
+
//
|
|
821
|
+
// Driven from onDidPaste rather than Monaco's own `formatOnPaste` option.
|
|
822
|
+
// That option has been on by default all along and never did anything here:
|
|
823
|
+
// its contribution is present and the event fires, but it declines to run
|
|
824
|
+
// the formatter, so pasted code kept whatever indentation it was copied
|
|
825
|
+
// with. onDidPaste is public API, fires reliably, and hands over the exact
|
|
826
|
+
// pasted range — which is also what makes the two cases distinguishable:
|
|
827
|
+
//
|
|
828
|
+
// * paste that fills the whole document (a blank file, or replacing all
|
|
829
|
+
// of it) is formatted as a document
|
|
830
|
+
// * a paste in the middle is formatted as a range, so Prettier reprints
|
|
831
|
+
// the smallest enclosing statement and the rest of the file is left
|
|
832
|
+
// byte-identical
|
|
833
|
+
//
|
|
834
|
+
// Either way the result comes back at the editor's own tab/space setting,
|
|
835
|
+
// which is the point: code copied in from a spaces project lands as tabs.
|
|
836
|
+
var pasteDisposable = editor.onDidPaste(function (e) {
|
|
837
|
+
var prefs = (typeof EditorStore !== 'undefined' && EditorStore.getState().editorPrefs) || {};
|
|
838
|
+
if (prefs.formatOnPaste === false) return;
|
|
839
|
+
|
|
840
|
+
var pasteModel = editor.getModel();
|
|
841
|
+
if (!pasteModel || pasteModel.isDisposed()) return;
|
|
842
|
+
|
|
843
|
+
var full = pasteModel.getFullModelRange();
|
|
844
|
+
var wholeDocument = e.range.startLineNumber <= full.startLineNumber &&
|
|
845
|
+
e.range.endLineNumber >= full.endLineNumber;
|
|
846
|
+
|
|
847
|
+
var action = editor.getAction(wholeDocument ? 'editor.action.formatDocument' : 'editor.action.formatSelection');
|
|
848
|
+
if (!action) return;
|
|
849
|
+
|
|
850
|
+
if (wholeDocument) {
|
|
851
|
+
action.run()["catch"](function () { /* no formatter, or unparseable — leave it */ });
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
// formatSelection works on the selection, so point it at the pasted range
|
|
856
|
+
// and put the cursor back where the paste left it.
|
|
857
|
+
var restore = editor.getSelections();
|
|
858
|
+
editor.setSelection(e.range);
|
|
859
|
+
action.run()["catch"](function () {})["finally"](function () {
|
|
860
|
+
if (restore && restore.length && !editor.getModel().isDisposed()) editor.setSelections(restore);
|
|
861
|
+
});
|
|
862
|
+
});
|
|
863
|
+
|
|
819
864
|
return {
|
|
820
865
|
dispose: function dispose() {
|
|
821
866
|
if (keydownDisposable) keydownDisposable.dispose();
|
|
822
867
|
if (emmetTabDisposable) emmetTabDisposable.dispose();
|
|
823
868
|
if (jsGotoMouseDisposable) jsGotoMouseDisposable.dispose();
|
|
824
869
|
if (jsGotoActionDisposable) jsGotoActionDisposable.dispose();
|
|
870
|
+
if (pasteDisposable) pasteDisposable.dispose();
|
|
825
871
|
contentDisposable.dispose();
|
|
826
872
|
}
|
|
827
873
|
};
|
|
@@ -853,6 +899,22 @@
|
|
|
853
899
|
jsx: monaco.languages.typescript.JsxEmit.React,
|
|
854
900
|
noUnusedLocals: true
|
|
855
901
|
});
|
|
902
|
+
// Hand JS formatting to Prettier alone.
|
|
903
|
+
//
|
|
904
|
+
// The TypeScript worker registers its own range formatter for
|
|
905
|
+
// 'javascript', and Monaco consults whichever provider it finds first.
|
|
906
|
+
// That gave one file two formatters that disagreed: the toolbar button
|
|
907
|
+
// went through Prettier and honoured the editor's tab settings, while
|
|
908
|
+
// Shift+Alt+F and format-on-paste got the TS worker's, which reprints at
|
|
909
|
+
// two spaces and never adds semicolons, ignoring every preference. Only
|
|
910
|
+
// formatting is switched off — completions, hovers, diagnostics, rename
|
|
911
|
+
// and the rest of the JS intelligence still come from the worker.
|
|
912
|
+
monaco.languages.typescript.javascriptDefaults.setModeConfiguration(
|
|
913
|
+
Object.assign({}, monaco.languages.typescript.javascriptDefaults.modeConfiguration, {
|
|
914
|
+
documentRangeFormattingEdits: false,
|
|
915
|
+
onTypeFormattingEdits: false
|
|
916
|
+
})
|
|
917
|
+
);
|
|
856
918
|
}
|
|
857
919
|
|
|
858
920
|
// ── React mini-UMD type declarations ────────────────────────────────────
|
|
@@ -1767,6 +1829,82 @@
|
|
|
1767
1829
|
}).catch(function () { return []; });
|
|
1768
1830
|
}
|
|
1769
1831
|
|
|
1832
|
+
// ── Prettier formatting providers ────────────────────────────────────────
|
|
1833
|
+
//
|
|
1834
|
+
// `formatOnPaste` has been on by default for a long time and did nothing
|
|
1835
|
+
// outside Ruby: Monaco acts on a paste only through a *range* formatting
|
|
1836
|
+
// provider, and none was registered. Registering one here is what makes
|
|
1837
|
+
// pasted blocks land correctly indented, and what formats the whole thing
|
|
1838
|
+
// when the paste fills an empty file (the pasted range is then the file).
|
|
1839
|
+
//
|
|
1840
|
+
// Prettier's own rangeStart/rangeEnd does the narrowing — it reprints the
|
|
1841
|
+
// smallest enclosing statement and leaves the rest byte-identical, so a
|
|
1842
|
+
// paste in the middle of a file does not reformat the file.
|
|
1843
|
+
var PRETTIER_LANGUAGE_PARSERS = {
|
|
1844
|
+
javascript: 'babel', json: 'json', css: 'css',
|
|
1845
|
+
scss: 'scss', less: 'less', html: 'html', markdown: 'markdown'
|
|
1846
|
+
};
|
|
1847
|
+
|
|
1848
|
+
// One edit spanning only what actually changed. A whole-document
|
|
1849
|
+
// replacement would work but throws away the cursor position and collapses
|
|
1850
|
+
// undo, which on every paste is very noticeable.
|
|
1851
|
+
function minimalEdit(model, oldText, newText) {
|
|
1852
|
+
if (oldText === newText) return [];
|
|
1853
|
+
var start = 0;
|
|
1854
|
+
var maxStart = Math.min(oldText.length, newText.length);
|
|
1855
|
+
while (start < maxStart && oldText.charCodeAt(start) === newText.charCodeAt(start)) start++;
|
|
1856
|
+
var oldEnd = oldText.length;
|
|
1857
|
+
var newEnd = newText.length;
|
|
1858
|
+
while (oldEnd > start && newEnd > start && oldText.charCodeAt(oldEnd - 1) === newText.charCodeAt(newEnd - 1)) {
|
|
1859
|
+
oldEnd--;
|
|
1860
|
+
newEnd--;
|
|
1861
|
+
}
|
|
1862
|
+
return [{
|
|
1863
|
+
range: monaco.Range.fromPositions(model.getPositionAt(start), model.getPositionAt(oldEnd)),
|
|
1864
|
+
text: newText.slice(start, newEnd)
|
|
1865
|
+
}];
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1868
|
+
function prettierEdits(model, range) {
|
|
1869
|
+
var parser = PRETTIER_LANGUAGE_PARSERS[model.getLanguageId()];
|
|
1870
|
+
if (!parser || typeof runPrettier !== 'function') return Promise.resolve([]);
|
|
1871
|
+
|
|
1872
|
+
var text = model.getValue();
|
|
1873
|
+
var prefs = (typeof EditorStore !== 'undefined' && EditorStore.getState().editorPrefs) || {};
|
|
1874
|
+
var extra = null;
|
|
1875
|
+
if (range) {
|
|
1876
|
+
extra = {
|
|
1877
|
+
rangeStart: model.getOffsetAt({ lineNumber: range.startLineNumber, column: range.startColumn }),
|
|
1878
|
+
rangeEnd: model.getOffsetAt({ lineNumber: range.endLineNumber, column: range.endColumn })
|
|
1879
|
+
};
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1882
|
+
return runPrettier(text, prefs, parser, extra)
|
|
1883
|
+
.then(function (formatted) {
|
|
1884
|
+
// The model can have moved on while Prettier was working.
|
|
1885
|
+
if (model.isDisposed() || model.getValue() !== text) return [];
|
|
1886
|
+
return minimalEdit(model, text, formatted);
|
|
1887
|
+
})
|
|
1888
|
+
// A paste of half an expression will not parse. Leaving it alone is the
|
|
1889
|
+
// right answer — the diagnostics path reports the syntax error.
|
|
1890
|
+
["catch"](function () { return []; });
|
|
1891
|
+
}
|
|
1892
|
+
|
|
1893
|
+
Object.keys(PRETTIER_LANGUAGE_PARSERS).forEach(function (languageId) {
|
|
1894
|
+
monaco.languages.registerDocumentRangeFormattingEditProvider(languageId, {
|
|
1895
|
+
displayName: 'Prettier',
|
|
1896
|
+
provideDocumentRangeFormattingEdits: function (model, range) {
|
|
1897
|
+
return prettierEdits(model, range);
|
|
1898
|
+
}
|
|
1899
|
+
});
|
|
1900
|
+
monaco.languages.registerDocumentFormattingEditProvider(languageId, {
|
|
1901
|
+
displayName: 'Prettier',
|
|
1902
|
+
provideDocumentFormattingEdits: function (model) {
|
|
1903
|
+
return prettierEdits(model, null);
|
|
1904
|
+
}
|
|
1905
|
+
});
|
|
1906
|
+
});
|
|
1907
|
+
|
|
1770
1908
|
// Parameter hints while typing a call's arguments.
|
|
1771
1909
|
monaco.languages.registerSignatureHelpProvider('ruby', {
|
|
1772
1910
|
signatureHelpTriggerCharacters: ['(', ','],
|
|
@@ -93,11 +93,25 @@ axios.interceptors.request.use(function (config) {
|
|
|
93
93
|
return config;
|
|
94
94
|
});
|
|
95
95
|
|
|
96
|
+
// A request we aborted ourselves also arrives here with no response, and the
|
|
97
|
+
// editor cancels constantly — every keystroke supersedes the previous search,
|
|
98
|
+
// every hover and completion provider aborts when the cursor moves on, and
|
|
99
|
+
// opening a file aborts its own prefetch. Counting those as unreachable took
|
|
100
|
+
// two cancellations to declare the server offline, so routine actions (creating
|
|
101
|
+
// a file, typing in search) flashed "Server offline" until the /ping probe
|
|
102
|
+
// one second later put it back. A cancellation says nothing about the server.
|
|
103
|
+
function _isCanceled(error) {
|
|
104
|
+
if (!error) return false;
|
|
105
|
+
if (axios.isCancel && axios.isCancel(error)) return true;
|
|
106
|
+
return error.code === 'ERR_CANCELED' || error.name === 'CanceledError' || error.name === 'AbortError';
|
|
107
|
+
}
|
|
108
|
+
|
|
96
109
|
axios.interceptors.response.use(function (response) {
|
|
97
110
|
ServerReachability.noteSuccess();
|
|
98
111
|
return response;
|
|
99
112
|
}, function (error) {
|
|
100
113
|
if (error && error.mbeditorSkipped) return Promise.reject(error);
|
|
114
|
+
if (_isCanceled(error)) return Promise.reject(error);
|
|
101
115
|
// No response object at all means the request never reached the server.
|
|
102
116
|
if (error && !error.response) ServerReachability.noteNetworkFailure();
|
|
103
117
|
else ServerReachability.noteSuccess();
|
|
@@ -32,17 +32,22 @@ module Mbeditor
|
|
|
32
32
|
end
|
|
33
33
|
|
|
34
34
|
# Expand path and confirm it's inside workspace_root.
|
|
35
|
-
#
|
|
36
|
-
#
|
|
37
|
-
#
|
|
38
|
-
#
|
|
35
|
+
#
|
|
36
|
+
# Containment is judged lexically, on the path as spelled. File.expand_path
|
|
37
|
+
# collapses "..", so no request can name anything outside the workspace, and
|
|
38
|
+
# an absolute path is rejected outright.
|
|
39
|
+
#
|
|
40
|
+
# Symlinks are followed, the way a file manager does: an app that links a
|
|
41
|
+
# shared config directory or a sibling engine into its tree gets those files
|
|
42
|
+
# opening like any other. The link itself has to already exist inside the
|
|
43
|
+
# workspace, which means a developer put it there — the sandbox is about
|
|
44
|
+
# what a request may *name*, not about second-guessing the checkout.
|
|
39
45
|
def resolve_path(raw)
|
|
40
46
|
return nil if raw.blank?
|
|
41
47
|
|
|
42
48
|
root = workspace_root.to_s
|
|
43
49
|
full = File.expand_path(raw.to_s, root)
|
|
44
50
|
return nil unless full.start_with?("#{root}/") || full == root
|
|
45
|
-
return nil unless SafePath.within?(root, full)
|
|
46
51
|
|
|
47
52
|
full
|
|
48
53
|
rescue Errno::EACCES, ArgumentError
|
|
@@ -22,7 +22,7 @@ module Mbeditor
|
|
|
22
22
|
root = workspace_root.to_s
|
|
23
23
|
if SearchReplaceService.rg_available?
|
|
24
24
|
run_rg(pattern, root, globs)
|
|
25
|
-
elsif
|
|
25
|
+
elsif SearchReplaceService.git_tier?(root)
|
|
26
26
|
run_git_grep(pattern, root, globs)
|
|
27
27
|
else
|
|
28
28
|
run_grep(pattern, root, globs)
|
|
@@ -155,9 +155,12 @@ module Mbeditor
|
|
|
155
155
|
# Resolve a file path safely within repo_path. Returns full path string or
|
|
156
156
|
# nil if the path escapes the root.
|
|
157
157
|
#
|
|
158
|
-
#
|
|
159
|
-
# inside the repo cannot escape it
|
|
160
|
-
# ApplicationController#resolve_path
|
|
158
|
+
# Resolves symlinks (via SafePath, including dangling ones) so that a
|
|
159
|
+
# symlink inside the repo cannot escape it. Deliberately stricter than
|
|
160
|
+
# ApplicationController#resolve_path, which follows links out of the
|
|
161
|
+
# workspace so linked-in files open: git has nothing to say about a file
|
|
162
|
+
# outside its own repo, so there is no feature to lose here.
|
|
163
|
+
# When repo_path is not a real directory
|
|
161
164
|
# (e.g. unit tests with synthetic roots) the symlink check is skipped, since
|
|
162
165
|
# there is nothing on disk to resolve and repo_path is server-controlled.
|
|
163
166
|
def resolve_path(repo_path, relative)
|
|
@@ -45,6 +45,19 @@ module Mbeditor
|
|
|
45
45
|
AvailabilityProbe.rg_command || "rg"
|
|
46
46
|
end
|
|
47
47
|
|
|
48
|
+
# Whether the git tier is usable on this workspace. The presence of a
|
|
49
|
+
# `.git` entry is not enough: a repo git refuses to open (dubious
|
|
50
|
+
# ownership under Docker, a `.git` file pointing at a gitdir that moved,
|
|
51
|
+
# a git binary missing from the server's PATH) still has one. `git grep`
|
|
52
|
+
# then fails, its stderr goes to /dev/null, and search returns instantly
|
|
53
|
+
# with nothing — silently, and with no way to tell it apart from "no
|
|
54
|
+
# matches". AvailabilityProbe.git runs `rev-parse --is-inside-work-tree`,
|
|
55
|
+
# which fails in exactly those cases, so a bad repo falls through to
|
|
56
|
+
# plain grep instead of to an empty result set.
|
|
57
|
+
def git_tier?(root)
|
|
58
|
+
File.exist?(File.join(root, ".git")) && AvailabilityProbe.git(root)
|
|
59
|
+
end
|
|
60
|
+
|
|
48
61
|
# Which backend a search on this workspace will actually use. Reported by
|
|
49
62
|
# GET /workspace: the difference between the tiers is 10-30x, so "why is
|
|
50
63
|
# search slow" needs to be answerable without reading the source.
|
|
@@ -245,7 +258,7 @@ module Mbeditor
|
|
|
245
258
|
|
|
246
259
|
def pick_tier(root)
|
|
247
260
|
return :rg if rg_available?
|
|
248
|
-
return :git if
|
|
261
|
+
return :git if git_tier?(root)
|
|
249
262
|
|
|
250
263
|
:grep
|
|
251
264
|
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "active_support/logger_silence"
|
|
4
|
+
require "logger"
|
|
4
5
|
require "uri"
|
|
5
6
|
|
|
6
7
|
module Mbeditor
|
|
@@ -18,7 +19,14 @@ module Mbeditor
|
|
|
18
19
|
if root_request?(env, path)
|
|
19
20
|
@app.call(env)
|
|
20
21
|
elsif mbeditor_request?(path) || cable_request?(path) || editor_asset_request?(env, path)
|
|
21
|
-
|
|
22
|
+
# Silence to WARN, not the default ERROR. The point is to hide Rails'
|
|
23
|
+
# own request lines ("Started GET ...", "Completed 200 ..."), which are
|
|
24
|
+
# INFO — but the default swallows everything below ERROR, including
|
|
25
|
+
# anything a host app logs from inside authenticate_with or
|
|
26
|
+
# user_name_callback. Those procs run within this request, so a
|
|
27
|
+
# developer debugging their own hook with Rails.logger.warn saw
|
|
28
|
+
# nothing at all and had no way to know why.
|
|
29
|
+
Rails.logger.silence(::Logger::WARN) { @app.call(env) }
|
|
22
30
|
else
|
|
23
31
|
@app.call(env)
|
|
24
32
|
end
|
data/lib/mbeditor/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: mbeditor
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.12.
|
|
4
|
+
version: 0.12.6
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Oliver Noonan
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-08-
|
|
11
|
+
date: 2026-08-05 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rails
|