mbeditor 0.13.0 → 0.13.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.
Files changed (29) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +66 -1
  3. data/app/assets/javascripts/mbeditor/application.js +0 -1
  4. data/app/assets/javascripts/mbeditor/collaboration_service.js +22 -2
  5. data/app/assets/javascripts/mbeditor/components/EditorPanel.js +11 -87
  6. data/app/assets/javascripts/mbeditor/components/ImportDialog.js +9 -1
  7. data/app/assets/javascripts/mbeditor/components/MbeditorApp.js +171 -205
  8. data/app/assets/javascripts/mbeditor/components/ProblemsPanel.js +35 -7
  9. data/app/assets/javascripts/mbeditor/components/QuickOpenDialog.js +97 -75
  10. data/app/assets/javascripts/mbeditor/editor_plugins.js +7 -1
  11. data/app/assets/javascripts/mbeditor/file_service.js +8 -18
  12. data/app/assets/javascripts/mbeditor/search_service.js +20 -2
  13. data/app/assets/javascripts/mbeditor/tab_manager.js +7 -4
  14. data/app/assets/stylesheets/mbeditor/editor.css +138 -67
  15. data/app/channels/mbeditor/collaboration_channel.rb +8 -2
  16. data/app/controllers/mbeditor/editors_controller.rb +8 -26
  17. data/app/services/mbeditor/collaboration_doc_store.rb +42 -3
  18. data/app/services/mbeditor/duplicate_content_scanner.rb +105 -0
  19. data/app/services/mbeditor/js_globals_service.rb +12 -1
  20. data/app/services/mbeditor/rubocop_run_service.rb +17 -5
  21. data/app/services/mbeditor/schema_service.rb +8 -2
  22. data/app/services/mbeditor/search_replace_service.rb +11 -2
  23. data/app/services/mbeditor/test_runner_service.rb +3 -56
  24. data/lib/mbeditor/configuration.rb +0 -8
  25. data/lib/mbeditor/route_map.rb +0 -1
  26. data/lib/mbeditor/version.rb +1 -1
  27. data/lib/tasks/mbeditor.rake +23 -0
  28. metadata +4 -3
  29. data/app/assets/javascripts/mbeditor/components/TestRunPanel.js +0 -312
@@ -112,6 +112,10 @@ var ProblemsPanel = (function () {
112
112
  var Panel = function ProblemsPanelComponent(_ref) {
113
113
  var onClose = _ref.onClose;
114
114
  var onOpenFile = _ref.onOpenFile;
115
+ // Called after `-a` has rewritten files on disk, so the app can re-read
116
+ // every open tab. Nothing else would: the write came from a subprocess,
117
+ // not from the editor, so the buffers and their markers are stale.
118
+ var onFilesRewritten = _ref.onFilesRewritten;
115
119
 
116
120
  var _problems = React.useState(collect);
117
121
  var problems = _problems[0], setProblems = _problems[1];
@@ -190,7 +194,10 @@ var ProblemsPanel = (function () {
190
194
  if (running) return;
191
195
  setRunning(mode);
192
196
  FileService.runRubocop(mode)
193
- .then(function (data) { setWorkspace(data); })
197
+ .then(function (data) {
198
+ setWorkspace(data);
199
+ if (mode === 'autocorrect' && data && data.ok && onFilesRewritten) onFilesRewritten();
200
+ })
194
201
  ["catch"](function (e) {
195
202
  var res = e && e.response && e.response.data;
196
203
  setWorkspace(res || { ok: false, error: (e && e.message) || 'RuboCop failed', files: [] });
@@ -283,6 +290,25 @@ var ProblemsPanel = (function () {
283
290
  }).filter(function (entry) { return entry.markers.length > 0; })
284
291
  : problems.byFile;
285
292
 
293
+ // The workspace run and the marker set are different scopes, but they render
294
+ // in one panel under one set of severity chips. Counting only the markers
295
+ // made the header disagree with its own body: click RuboCop, get 400
296
+ // offenses listed under chips reading 0 / 0 / 0, and hiding "warning" left
297
+ // every workspace warning on screen. The chips describe what the panel is
298
+ // showing, so they have to cover both — and the filter has to reach both.
299
+ var wsFiles = (workspace && workspace.files) || [];
300
+ var wsCounts = { error: 0, warning: 0, info: 0 };
301
+ var wsShown = [];
302
+ wsFiles.forEach(function (file) {
303
+ var kept = [];
304
+ file.offenses.forEach(function (o) {
305
+ var kind = COP_SEVERITY_KIND[o.severity] || 'info';
306
+ wsCounts[kind] += 1;
307
+ if (severities[kind]) kept.push(o);
308
+ });
309
+ if (kept.length) wsShown.push({ path: file.path, offenses: kept });
310
+ });
311
+
286
312
  var total = problems.errors.length + problems.warnings.length + problems.infos.length;
287
313
 
288
314
  return React.createElement(
@@ -304,9 +330,9 @@ var ProblemsPanel = (function () {
304
330
  'div',
305
331
  { className: 'ide-problems-severity-filter' },
306
332
  [
307
- { kind: 'error', count: problems.errors.length },
308
- { kind: 'warning', count: problems.warnings.length },
309
- { kind: 'info', count: problems.infos.length }
333
+ { kind: 'error', count: problems.errors.length + wsCounts.error },
334
+ { kind: 'warning', count: problems.warnings.length + wsCounts.warning },
335
+ { kind: 'info', count: problems.infos.length + wsCounts.info }
310
336
  ].map(function (s) {
311
337
  var on = severities[s.kind];
312
338
  return React.createElement(
@@ -342,7 +368,9 @@ var ProblemsPanel = (function () {
342
368
  disabled: !!running || !workspace || !workspace.correctable,
343
369
  title: workspace && workspace.correctable
344
370
  ? 'Safe-autocorrect ' + workspace.correctable + ' offense(s) and rerun'
345
- : 'Run RuboCop first',
371
+ : workspace
372
+ ? 'Nothing left that safe autocorrect can fix'
373
+ : 'Run RuboCop first',
346
374
  onClick: function () { runRubocop('autocorrect'); }
347
375
  },
348
376
  React.createElement('i', { className: running === 'autocorrect' ? 'fas fa-spinner fa-spin' : 'fas fa-magic' }),
@@ -378,9 +406,9 @@ var ProblemsPanel = (function () {
378
406
  { className: 'ide-problems-workspace' },
379
407
  workspace.error
380
408
  ? React.createElement('div', { className: 'ide-problems-empty' }, 'RuboCop: ' + workspace.error)
381
- : (workspace.files || []).length === 0
409
+ : wsFiles.length === 0
382
410
  ? React.createElement('div', { className: 'ide-problems-empty' }, 'RuboCop found no offenses in the workspace')
383
- : workspace.files.map(function (file) {
411
+ : wsShown.map(function (file) {
384
412
  return React.createElement(
385
413
  'div',
386
414
  { className: 'ide-problems-file', key: 'ws:' + file.path },
@@ -23,6 +23,100 @@ function saveFavourites(list) {
23
23
  try { localStorage.setItem(FAVS_KEY, JSON.stringify(list)); } catch (e) {}
24
24
  }
25
25
 
26
+ // ── Ranking ────────────────────────────────────────────────────────────────
27
+ // Module scope, not component scope, so `rankResults` can be exercised without
28
+ // rendering — see script/check-quick-open-ranking.mjs.
29
+
30
+ // Priority tier for a file path: lower = shown first.
31
+ // Order: controller > model > helper > concern > view > job > other > noise
32
+ function getFilePriority(path) {
33
+ var p = (path || '').toLowerCase();
34
+ if (p.indexOf('/controllers/') >= 0) return 1;
35
+ if (p.indexOf('/models/') >= 0) return 2;
36
+ if (p.indexOf('/helpers/') >= 0) return 3;
37
+ if (p.indexOf('/concerns/') >= 0) return 4;
38
+ if (p.indexOf('/views/') >= 0) return 5;
39
+ if (p.indexOf('/jobs/') >= 0) return 6;
40
+ // Deprioritise: migrations, schema, compiled assets, vendor, lock files
41
+ if (p.indexOf('/migrate/') >= 0 || p.indexOf('schema.rb') >= 0) return 90;
42
+ if (p.indexOf('/public/') >= 0 || p.indexOf('/vendor/') >= 0) return 91;
43
+ if (p.slice(-7) === '.min.js' || p.slice(-8) === '.min.css' ||
44
+ p.slice(-4) === '.map' || p.slice(-5) === '.lock') return 92;
45
+ return 50;
46
+ }
47
+
48
+ // Recently opened files, most recent first, as a path -> rank lookup. Built
49
+ // once per query rather than per comparison, since the sort calls this for
50
+ // every pair.
51
+ function recentRanks() {
52
+ var ranks = {};
53
+ var recent = (typeof TabManager !== 'undefined' && TabManager.getRecentFiles)
54
+ ? TabManager.getRecentFiles() : [];
55
+ recent.forEach(function (entry, index) {
56
+ var path = typeof entry === 'string' ? entry : (entry && entry.path);
57
+ if (path && !(path in ranks)) ranks[path] = index;
58
+ });
59
+ return ranks;
60
+ }
61
+
62
+ // Match relevance within a priority tier:
63
+ // exact basename > basename prefix > basename substring > path substring > fuzzy-only.
64
+ // FUZZY_ONLY is the bucket everything MiniSearch matched approximately lands
65
+ // in — a typo'd or transposed query hits nothing literally, so an exact match
66
+ // can never be demoted by loosening the index search.
67
+ var FUZZY_ONLY = 4;
68
+ function getMatchRelevance(result, q) {
69
+ if (!q) return FUZZY_ONLY;
70
+ var name = (result.name || (result.path || '').split('/').pop() || '').toLowerCase();
71
+ var lq = q.toLowerCase();
72
+ if (name === lq) return 0;
73
+ if (name.slice(0, lq.length) === lq) return 1;
74
+ if (name.indexOf(lq) >= 0) return 2;
75
+ if ((result.path || '').toLowerCase().indexOf(lq) >= 0) return 3;
76
+ return FUZZY_ONLY;
77
+ }
78
+
79
+ // Filter SearchService hits by type and order them for display.
80
+ // Precedence:
81
+ // 1. files before folders — a folder is never what Ctrl+P is for, and as a
82
+ // tier below match quality an exactly-named folder used to outrank every
83
+ // file that matched
84
+ // 2. match quality (see getMatchRelevance), so a worse match can never jump
85
+ // the queue however recently it was opened
86
+ // 3. MiniSearch's own score, but only within the fuzzy-only bucket: nothing
87
+ // there matched literally, so edit distance is the only signal for which
88
+ // near-miss the user meant
89
+ // 4. how recently the file was opened, most recent first
90
+ // 5. the static file-type tier (controller > model > … > noise)
91
+ //
92
+ // Recency sits above the type tier deliberately: when two files match a query
93
+ // equally well, the one you were just working in is almost always the one you
94
+ // meant, and that beats a guess made from the directory name. Files never
95
+ // opened all tie here and fall through to the type tier, which is what orders
96
+ // the bulk of a cold result list.
97
+ //
98
+ // JS sort is stable in modern engines, so MiniSearch's own relevance order
99
+ // remains the final tiebreaker.
100
+ function rankResults(res, query, showFolders) {
101
+ var filtered = showFolders ? res.slice() : res.filter(function (r) { return r.type !== 'dir'; });
102
+ var ranks = recentRanks();
103
+ var NEVER_OPENED = Infinity;
104
+ filtered.sort(function (a, b) {
105
+ var aDir = a.type === 'dir' ? 1 : 0;
106
+ var bDir = b.type === 'dir' ? 1 : 0;
107
+ if (aDir !== bDir) return aDir - bDir;
108
+ var aRelevance = getMatchRelevance(a, query);
109
+ var bRelevance = getMatchRelevance(b, query);
110
+ if (aRelevance !== bRelevance) return aRelevance - bRelevance;
111
+ if (aRelevance === FUZZY_ONLY && a.score !== b.score) return (b.score || 0) - (a.score || 0);
112
+ var aRecent = a.path in ranks ? ranks[a.path] : NEVER_OPENED;
113
+ var bRecent = b.path in ranks ? ranks[b.path] : NEVER_OPENED;
114
+ if (aRecent !== bRecent) return aRecent - bRecent;
115
+ return getFilePriority(a.path) - getFilePriority(b.path);
116
+ });
117
+ return filtered;
118
+ }
119
+
26
120
  // ── Component ──────────────────────────────────────────────────────────────
27
121
 
28
122
  var QuickOpenDialog = function QuickOpenDialog(_ref) {
@@ -76,49 +170,6 @@ var QuickOpenDialog = function QuickOpenDialog(_ref) {
76
170
  if (inputRef.current) inputRef.current.focus();
77
171
  };
78
172
 
79
- // Priority tier for a file path: lower = shown first.
80
- // Order: controller > model > helper > concern > view > job > other > noise
81
- function getFilePriority(path) {
82
- var p = (path || '').toLowerCase();
83
- if (p.indexOf('/controllers/') >= 0) return 1;
84
- if (p.indexOf('/models/') >= 0) return 2;
85
- if (p.indexOf('/helpers/') >= 0) return 3;
86
- if (p.indexOf('/concerns/') >= 0) return 4;
87
- if (p.indexOf('/views/') >= 0) return 5;
88
- if (p.indexOf('/jobs/') >= 0) return 6;
89
- // Deprioritise: migrations, schema, compiled assets, vendor, lock files
90
- if (p.indexOf('/migrate/') >= 0 || p.indexOf('schema.rb') >= 0) return 90;
91
- if (p.indexOf('/public/') >= 0 || p.indexOf('/vendor/') >= 0) return 91;
92
- if (p.slice(-7) === '.min.js' || p.slice(-8) === '.min.css' ||
93
- p.slice(-4) === '.map' || p.slice(-5) === '.lock') return 92;
94
- return 50;
95
- }
96
-
97
- // Recently opened files, most recent first, as a path -> rank lookup. Built
98
- // once per query rather than per comparison, since the sort calls this for
99
- // every pair.
100
- function recentRanks() {
101
- var ranks = {};
102
- var recent = (typeof TabManager !== 'undefined' && TabManager.getRecentFiles)
103
- ? TabManager.getRecentFiles() : [];
104
- recent.forEach(function (entry, index) {
105
- var path = typeof entry === 'string' ? entry : (entry && entry.path);
106
- if (path && !(path in ranks)) ranks[path] = index;
107
- });
108
- return ranks;
109
- }
110
-
111
- // Match relevance within a priority tier: exact basename > prefix > substring > other.
112
- function getMatchRelevance(result, q) {
113
- if (!q) return 3;
114
- var name = (result.name || (result.path || '').split('/').pop() || '').toLowerCase();
115
- var lq = q.toLowerCase();
116
- if (name === lq) return 0;
117
- if (name.slice(0, lq.length) === lq) return 1;
118
- if (name.indexOf(lq) >= 0) return 2;
119
- return 3;
120
- }
121
-
122
173
  var getQuickOpenIcon = function getQuickOpenIcon(path, name, type) {
123
174
  if (type === 'dir') {
124
175
  return React.createElement('i', { className: 'fas fa-folder quick-open-result-icon quick-open-folder-icon', 'aria-hidden': 'true' });
@@ -143,37 +194,7 @@ var QuickOpenDialog = function QuickOpenDialog(_ref) {
143
194
  // path in the workspace ran on every keystroke, between the keypress and
144
195
  // the character appearing.
145
196
  var timer = setTimeout(function () {
146
- var res = SearchService.searchFiles(query);
147
- // Filter by type: always include files; include dirs only when showFolders is on
148
- var filtered = showFolders ? res : res.filter(function(r) { return r.type !== 'dir'; });
149
- // Sort, in order of precedence:
150
- // 1. match quality — exact basename > prefix > substring > other, so a
151
- // worse match can never jump the queue however recently it was opened
152
- // 2. how recently the file was opened, most recent first
153
- // 3. the static file-type tier (controller > model > … > noise)
154
- //
155
- // Recency sits above the type tier deliberately: when two files match a
156
- // query equally well, the one you were just working in is almost always
157
- // the one you meant, and that beats a guess made from the directory name.
158
- // Files never opened all tie here and fall through to the type tier, which
159
- // is what orders the bulk of a cold result list.
160
- //
161
- // Directories keep their +100 penalty inside the type tier so files still
162
- // come first. JS sort is stable in modern engines, so MiniSearch's own
163
- // relevance order remains the final tiebreaker.
164
- var ranks = recentRanks();
165
- var NEVER_OPENED = Infinity;
166
- filtered.sort(function(a, b) {
167
- var aRelevance = getMatchRelevance(a, query);
168
- var bRelevance = getMatchRelevance(b, query);
169
- if (aRelevance !== bRelevance) return aRelevance - bRelevance;
170
- var aRecent = a.path in ranks ? ranks[a.path] : NEVER_OPENED;
171
- var bRecent = b.path in ranks ? ranks[b.path] : NEVER_OPENED;
172
- if (aRecent !== bRecent) return aRecent - bRecent;
173
- var aPriority = getFilePriority(a.path) + (a.type === 'dir' ? 100 : 0);
174
- var bPriority = getFilePriority(b.path) + (b.type === 'dir' ? 100 : 0);
175
- return aPriority - bPriority;
176
- });
197
+ var filtered = rankResults(SearchService.searchFiles(query), query, showFolders);
177
198
  setResults(filtered.slice(0, 200));
178
199
  setSelectedIndex(0);
179
200
  }, 120);
@@ -395,7 +416,7 @@ var QuickOpenDialog = function QuickOpenDialog(_ref) {
395
416
  }),
396
417
  results.length === 0 && React.createElement(
397
418
  'div',
398
- { style: { padding: '12px 16px', color: '#666', fontSize: '12px' } },
419
+ { style: { padding: '14px 18px', color: '#666', fontSize: '14px' } },
399
420
  'No matching files.'
400
421
  )
401
422
  )
@@ -404,4 +425,5 @@ var QuickOpenDialog = function QuickOpenDialog(_ref) {
404
425
  );
405
426
  };
406
427
 
428
+ QuickOpenDialog.rankResults = rankResults;
407
429
  window.QuickOpenDialog = QuickOpenDialog;
@@ -1484,7 +1484,13 @@
1484
1484
  cases: {
1485
1485
  '$2~(?i:SQL)': { token: 'string.heredoc.delimiter', next: '@heredocEmbedded.$2', nextEmbedded: 'sql' },
1486
1486
  '$2~(?i:HTML?|ERB)': { token: 'string.heredoc.delimiter', next: '@heredocEmbedded.$2', nextEmbedded: 'html' },
1487
- '$2~(?i:JS|JAVASCRIPT)': { token: 'string.heredoc.delimiter', next: '@heredocEmbedded.$2', nextEmbedded: 'javascript' },
1487
+ // JSX rides the javascript tokenizer: Monaco registers no
1488
+ // separate jsx language, and its TS-derived grammar lexes JSX
1489
+ // bodies fine. The guard is anchored (^...$), so plain `JS`
1490
+ // never matched `JSX` and it fell through to the generic
1491
+ // heredoc — the whole block painted as one string.
1492
+ '$2~(?i:JSX?|JAVASCRIPT)': { token: 'string.heredoc.delimiter', next: '@heredocEmbedded.$2', nextEmbedded: 'javascript' },
1493
+ '$2~(?i:TSX?|TYPESCRIPT)': { token: 'string.heredoc.delimiter', next: '@heredocEmbedded.$2', nextEmbedded: 'typescript' },
1488
1494
  '$2~(?i:CSS)': { token: 'string.heredoc.delimiter', next: '@heredocEmbedded.$2', nextEmbedded: 'css' },
1489
1495
  '$2~(?i:SCSS)': { token: 'string.heredoc.delimiter', next: '@heredocEmbedded.$2', nextEmbedded: 'scss' },
1490
1496
  '$2~(?i:JSON)': { token: 'string.heredoc.delimiter', next: '@heredocEmbedded.$2', nextEmbedded: 'json' },
@@ -276,30 +276,21 @@ var FileService = (function () {
276
276
 
277
277
  // line (1-based, optional) narrows the run to the single test at that line;
278
278
  // the server ignores it unless `path` IS the test file.
279
- // Server-reported ceilings (seconds), seeded from /workspace. The request
280
- // timeout is derived from them rather than hard-coded: two independent
279
+ // Server-reported ceiling (seconds), seeded from /workspace. The request
280
+ // timeout is derived from it rather than hard-coded: two independent
281
281
  // numbers meant that raising config.test_timeout past the client's own cap
282
282
  // made the browser abort a run the server was still executing, and the user
283
283
  // saw a generic network error instead of the server's message.
284
- var testTimeouts = { test: 180, testAll: 1800 };
285
- function setTestTimeouts(t) {
286
- if (t && t.test > 0) testTimeouts.test = t.test;
287
- if (t && t.testAll > 0) testTimeouts.testAll = t.testAll;
288
- }
289
- // Margin for boot, JSON encoding and the trip back, so the server's own
290
- // timeout is always the one that fires first and reports why.
291
- function requestTimeout(seconds) { return (seconds + 30) * 1000; }
284
+ var testTimeout = 180;
285
+ function setTestTimeout(seconds) { if (seconds > 0) testTimeout = seconds; }
292
286
 
293
287
  function runTests(path, line) {
294
288
  var payload = { path: path };
295
289
  if (line) payload.line = line;
290
+ // Margin for boot, JSON encoding and the trip back, so the server's own
291
+ // timeout is always the one that fires first and reports why.
296
292
  return axios.post(window.mbeditorBasePath() + '/test', payload,
297
- { timeout: requestTimeout(testTimeouts.test) }).then(function(res) { return res.data; });
298
- }
299
-
300
- function runAllTests() {
301
- return axios.post(window.mbeditorBasePath() + '/test_all', {},
302
- { timeout: requestTimeout(testTimeouts.testAll) }).then(function(res) { return res.data; });
293
+ { timeout: (testTimeout + 30) * 1000 }).then(function(res) { return res.data; });
303
294
  }
304
295
 
305
296
  function ping() {
@@ -585,8 +576,7 @@ var FileService = (function () {
585
576
  rubyRename: rubyRename,
586
577
  getModelGraph: getModelGraph,
587
578
  runRubocop: runRubocop,
588
- runAllTests: runAllTests,
589
- setTestTimeouts: setTestTimeouts,
579
+ setTestTimeout: setTestTimeout,
590
580
  getExceptions: getExceptions,
591
581
  clearExceptions: clearExceptions,
592
582
  getRelatedFiles: getRelatedFiles,
@@ -79,13 +79,31 @@ var SearchService = (function () {
79
79
  });
80
80
  }
81
81
 
82
+ // Fuzziness per term: edit distance is `fuzzy * term.length` inside
83
+ // MiniSearch, so short terms get none (at 3 chars every 3-letter token in the
84
+ // tree is within one edit and the list becomes noise) and longer ones get
85
+ // roughly one typo per 4 characters.
86
+ function _fuzzyFor(term) {
87
+ return term.length > 3 ? 0.25 : false;
88
+ }
89
+
82
90
  // Search files (and optionally folders) in the local MiniSearch index.
91
+ // Fuzzy + prefix, so a misspelled or half-typed query still finds the file;
92
+ // ranking back in QuickOpenDialog puts exact matches first, so loosening the
93
+ // match here cannot demote one.
83
94
  // Also performs a case-insensitive substring scan so that partial-word
84
- // queries like "project" reliably find "projects_controller.rb".
95
+ // queries like "project" reliably find "projects_controller.rb" — the
96
+ // tokenizer splits on punctuation, so a query spanning a separator
97
+ // ("models/user") tokenizes into terms no single token can satisfy.
85
98
  // Returns merged results; MiniSearch scored entries come first.
86
99
  function searchFiles(query) {
87
100
  if (!query) return [];
88
- var msResults = _miniSearch.search(query, { prefix: true, fuzzy: false, combineWith: 'AND' });
101
+ var msResults = _miniSearch.search(query, {
102
+ prefix: true,
103
+ fuzzy: _fuzzyFor,
104
+ combineWith: 'AND',
105
+ boost: { name: 2 }
106
+ });
89
107
  // Substring fallback — catch anything MiniSearch missed
90
108
  var q = query.toLowerCase();
91
109
  var msIds = new Set(msResults.map(function(r) { return r.id; }));
@@ -169,7 +169,10 @@ var TabManager = (function () {
169
169
  openTab(target.path, target.name, target.line, null, false, target.col);
170
170
  }
171
171
 
172
- function openTab(path, name, line, forcePaneId, isSoftOpen, col) {
172
+ // col/endCol are the 1-based bounds of the thing being jumped to. Pass both
173
+ // and the editor selects it, leaving the cursor at its end; pass col alone
174
+ // and it just parks the cursor there.
175
+ function openTab(path, name, line, forcePaneId, isSoftOpen, col, endCol) {
173
176
  if (line) _jumpOrigin = _snapshotPosition() || _jumpOrigin;
174
177
  var state = EditorStore.getState();
175
178
  var paneId = forcePaneId || state.focusedPaneId;
@@ -191,7 +194,7 @@ var TabManager = (function () {
191
194
  var existing = pane.tabs.find(function(t) { return t.path === path; });
192
195
 
193
196
  if (existing) {
194
- if (line) _updateTab(paneId, path, { gotoLine: line, gotoCol: col || null });
197
+ if (line) _updateTab(paneId, path, { gotoLine: line, gotoCol: col || null, gotoEndCol: endCol || null });
195
198
  switchTab(paneId, path);
196
199
  if (_isMarkdownPath(path)) {
197
200
  _ensureMarkdownPreview(paneId, path, existing.name || name, existing.content || "");
@@ -217,7 +220,7 @@ var TabManager = (function () {
217
220
  isSoftOpen: isSoftOpen ? true : false,
218
221
  loading: true
219
222
  };
220
- if (line) { newTab.gotoLine = line; newTab.gotoCol = col || null; }
223
+ if (line) { newTab.gotoLine = line; newTab.gotoCol = col || null; newTab.gotoEndCol = endCol || null; }
221
224
 
222
225
  var newPanes = state.panes.map(function(p) {
223
226
  if (p.id === paneId) {
@@ -699,7 +702,7 @@ var TabManager = (function () {
699
702
  }
700
703
 
701
704
  function clearGotoLine(paneId, path) {
702
- _updateTab(paneId, path, { gotoLine: null, gotoCol: null });
705
+ _updateTab(paneId, path, { gotoLine: null, gotoCol: null, gotoEndCol: null });
703
706
  }
704
707
 
705
708
  // VS Code-style scratch buffer: a tab with no file behind it. Nothing is