Commit 562c27f5 authored by Enrique Alcántara's avatar Enrique Alcántara

Merge branch '198605-monaco-blob' into 'master'

Replace ACE with Monaco for Blob editing/creation

Closes #198605

See merge request gitlab-org/gitlab!34677
parents ad8945b5 a6094c42
import { __ } from '~/locale';
export const BLOB_EDITOR_ERROR = __('An error occurred while rendering the editor');
export const BLOB_PREVIEW_ERROR = __('An error occurred previewing the blob');
...@@ -3,39 +3,75 @@ ...@@ -3,39 +3,75 @@
import $ from 'jquery'; import $ from 'jquery';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import createFlash from '~/flash'; import createFlash from '~/flash';
import { __ } from '~/locale'; import { BLOB_EDITOR_ERROR, BLOB_PREVIEW_ERROR } from './constants';
import TemplateSelectorMediator from '../blob/file_template_mediator'; import TemplateSelectorMediator from '../blob/file_template_mediator';
import getModeByFileExtension from '~/lib/utils/ace_utils'; import getModeByFileExtension from '~/lib/utils/ace_utils';
import { addEditorMarkdownListeners } from '~/lib/utils/text_markdown'; import { addEditorMarkdownListeners } from '~/lib/utils/text_markdown';
const monacoEnabled = window?.gon?.features?.monacoBlobs;
export default class EditBlob { export default class EditBlob {
// The options object has: // The options object has:
// assetsPath, filePath, currentAction, projectId, isMarkdown // assetsPath, filePath, currentAction, projectId, isMarkdown
constructor(options) { constructor(options) {
this.options = options; this.options = options;
this.configureAceEditor(); const { isMarkdown } = this.options;
Promise.resolve()
.then(() => {
return monacoEnabled ? this.configureMonacoEditor() : this.configureAceEditor();
})
.then(() => {
this.initModePanesAndLinks(); this.initModePanesAndLinks();
this.initSoftWrap();
this.initFileSelectors(); this.initFileSelectors();
this.initSoftWrap();
if (isMarkdown) {
addEditorMarkdownListeners(this.editor);
}
this.editor.focus();
})
.catch(() => createFlash(BLOB_EDITOR_ERROR));
}
configureMonacoEditor() {
return import(/* webpackChunkName: 'monaco_editor_lite' */ '~/editor/editor_lite').then(
EditorModule => {
const EditorLite = EditorModule.default;
const editorEl = document.getElementById('editor');
const fileNameEl =
document.getElementById('file_path') || document.getElementById('file_name');
const fileContentEl = document.getElementById('file-content');
const form = document.querySelector('.js-edit-blob-form');
this.editor = new EditorLite();
this.editor.createInstance({
el: editorEl,
blobPath: fileNameEl.value,
blobContent: editorEl.innerText,
});
fileNameEl.addEventListener('change', () => {
this.editor.updateModelLanguage(fileNameEl.value);
});
form.addEventListener('submit', () => {
fileContentEl.value = this.editor.getValue();
});
},
);
} }
configureAceEditor() { configureAceEditor() {
const { filePath, assetsPath, isMarkdown } = this.options; const { filePath, assetsPath } = this.options;
ace.config.set('modePath', `${assetsPath}/ace`); ace.config.set('modePath', `${assetsPath}/ace`);
ace.config.loadModule('ace/ext/searchbox'); ace.config.loadModule('ace/ext/searchbox');
ace.config.loadModule('ace/ext/modelist'); ace.config.loadModule('ace/ext/modelist');
this.editor = ace.edit('editor'); this.editor = ace.edit('editor');
if (isMarkdown) {
addEditorMarkdownListeners(this.editor);
}
// This prevents warnings re: automatic scrolling being logged // This prevents warnings re: automatic scrolling being logged
this.editor.$blockScrolling = Infinity; this.editor.$blockScrolling = Infinity;
this.editor.focus();
if (filePath) { if (filePath) {
this.editor.getSession().setMode(getModeByFileExtension(filePath)); this.editor.getSession().setMode(getModeByFileExtension(filePath));
} }
...@@ -81,7 +117,7 @@ export default class EditBlob { ...@@ -81,7 +117,7 @@ export default class EditBlob {
currentPane.empty().append(data); currentPane.empty().append(data);
currentPane.renderGFM(); currentPane.renderGFM();
}) })
.catch(() => createFlash(__('An error occurred previewing the blob'))); .catch(() => createFlash(BLOB_PREVIEW_ERROR));
} }
this.$toggleButton.show(); this.$toggleButton.show();
...@@ -90,14 +126,19 @@ export default class EditBlob { ...@@ -90,14 +126,19 @@ export default class EditBlob {
} }
initSoftWrap() { initSoftWrap() {
this.isSoftWrapped = false; this.isSoftWrapped = Boolean(monacoEnabled);
this.$toggleButton = $('.soft-wrap-toggle'); this.$toggleButton = $('.soft-wrap-toggle');
this.$toggleButton.toggleClass('soft-wrap-active', this.isSoftWrapped);
this.$toggleButton.on('click', () => this.toggleSoftWrap()); this.$toggleButton.on('click', () => this.toggleSoftWrap());
} }
toggleSoftWrap() { toggleSoftWrap() {
this.isSoftWrapped = !this.isSoftWrapped; this.isSoftWrapped = !this.isSoftWrapped;
this.$toggleButton.toggleClass('soft-wrap-active', this.isSoftWrapped); this.$toggleButton.toggleClass('soft-wrap-active', this.isSoftWrapped);
if (monacoEnabled) {
this.editor.updateOptions({ wordWrap: this.isSoftWrapped ? 'on' : 'off' });
} else {
this.editor.getSession().setUseWrapMode(this.isSoftWrapped); this.editor.getSession().setUseWrapMode(this.isSoftWrapped);
} }
}
} }
import { editor as monacoEditor, languages as monacoLanguages, Uri } from 'monaco-editor'; import { editor as monacoEditor, languages as monacoLanguages, Position, Uri } from 'monaco-editor';
import { DEFAULT_THEME, themes } from '~/ide/lib/themes'; import { DEFAULT_THEME, themes } from '~/ide/lib/themes';
import languages from '~/ide/lib/languages'; import languages from '~/ide/lib/languages';
import { defaultEditorOptions } from '~/ide/lib/editor_options'; import { defaultEditorOptions } from '~/ide/lib/editor_options';
...@@ -70,6 +70,22 @@ export default class Editor { ...@@ -70,6 +70,22 @@ export default class Editor {
} }
getValue() { getValue() {
return this.model.getValue(); return this.instance.getValue();
}
setValue(val) {
this.instance.setValue(val);
}
focus() {
this.instance.focus();
}
navigateFileStart() {
this.instance.setPosition(new Position(1, 1));
}
updateOptions(options = {}) {
this.instance.updateOptions(options);
} }
} }
...@@ -40,7 +40,7 @@ ...@@ -40,7 +40,7 @@
= select_tag :encoding, options_for_select([ "base64", "text" ], "text"), class: 'select2', tabindex: '-1' = select_tag :encoding, options_for_select([ "base64", "text" ], "text"), class: 'select2', tabindex: '-1'
.file-editor.code .file-editor.code
%pre.js-edit-mode-pane.qa-editor#editor= params[:content] || local_assigns[:blob_data] %pre.js-edit-mode-pane.qa-editor#editor{ data: { 'editor-loading': true } }= params[:content] || local_assigns[:blob_data]
- if local_assigns[:path] - if local_assigns[:path]
.js-edit-mode-pane#preview.hide .js-edit-mode-pane#preview.hide
.center .center
......
- breadcrumb_title "Repository" - breadcrumb_title "Repository"
- page_title "Edit", @blob.path, @ref - page_title "Edit", @blob.path, @ref
- content_for :page_specific_javascripts do - unless Feature.enabled?(:monaco_blobs)
- content_for :page_specific_javascripts do
= page_specific_javascript_tag('lib/ace.js') = page_specific_javascript_tag('lib/ace.js')
- if @conflict - if @conflict
......
- breadcrumb_title "Repository" - breadcrumb_title "Repository"
- page_title "New File", @path.presence, @ref - page_title "New File", @path.presence, @ref
- content_for :page_specific_javascripts do - unless Feature.enabled?(:monaco_blobs)
- content_for :page_specific_javascripts do
= page_specific_javascript_tag('lib/ace.js') = page_specific_javascript_tag('lib/ace.js')
.editor-title-row .editor-title-row
%h3.page-title.blob-new-page-title %h3.page-title.blob-new-page-title
New file New file
......
...@@ -2446,6 +2446,9 @@ msgstr "" ...@@ -2446,6 +2446,9 @@ msgstr ""
msgid "An error occurred while rendering preview broadcast message" msgid "An error occurred while rendering preview broadcast message"
msgstr "" msgstr ""
msgid "An error occurred while rendering the editor"
msgstr ""
msgid "An error occurred while reordering issues." msgid "An error occurred while reordering issues."
msgstr "" msgstr ""
......
...@@ -37,7 +37,7 @@ RSpec.describe 'a maintainer edits files on a source-branch of an MR from a fork ...@@ -37,7 +37,7 @@ RSpec.describe 'a maintainer edits files on a source-branch of an MR from a fork
end end
it 'allows committing to the source branch' do it 'allows committing to the source branch' do
find('.ace_text-input', visible: false).send_keys('Updated the readme') execute_script("monaco.editor.getModels()[0].setValue('Updated the readme')")
click_button 'Commit changes' click_button 'Commit changes'
wait_for_requests wait_for_requests
......
...@@ -36,8 +36,7 @@ RSpec.describe 'Editing file blob', :js do ...@@ -36,8 +36,7 @@ RSpec.describe 'Editing file blob', :js do
def fill_editor(content: 'class NextFeature\\nend\\n') def fill_editor(content: 'class NextFeature\\nend\\n')
wait_for_requests wait_for_requests
find('#editor') execute_script("monaco.editor.getModels()[0].setValue('#{content}')")
execute_script("ace.edit('editor').setValue('#{content}')")
end end
context 'from MR diff' do context 'from MR diff' do
...@@ -67,6 +66,15 @@ RSpec.describe 'Editing file blob', :js do ...@@ -67,6 +66,15 @@ RSpec.describe 'Editing file blob', :js do
expect(find_by_id('file_path').value).to eq('ci/.gitlab-ci.yml') expect(find_by_id('file_path').value).to eq('ci/.gitlab-ci.yml')
end end
it 'updating file path updates syntax highlighting' do
visit project_edit_blob_path(project, tree_join(branch, readme_file_path))
expect(find('#editor')['data-mode-id']).to eq('markdown')
find('#file_path').send_keys('foo.txt') do
expect(find('#editor')['data-mode-id']).to eq('plaintext')
end
end
context 'from blob file path' do context 'from blob file path' do
before do before do
stub_feature_flags(code_navigation: false) stub_feature_flags(code_navigation: false)
......
...@@ -16,8 +16,7 @@ RSpec.describe 'User creates blob in new project', :js do ...@@ -16,8 +16,7 @@ RSpec.describe 'User creates blob in new project', :js do
it 'allows the user to add a new file' do it 'allows the user to add a new file' do
click_link 'New file' click_link 'New file'
find('#editor') execute_script("monaco.editor.getModels()[0].setValue('Hello world')")
execute_script('ace.edit("editor").setValue("Hello world")')
fill_in(:file_name, with: 'dummy-file') fill_in(:file_name, with: 'dummy-file')
......
...@@ -32,6 +32,8 @@ RSpec.describe 'User follows pipeline suggest nudge spec when feature is enabled ...@@ -32,6 +32,8 @@ RSpec.describe 'User follows pipeline suggest nudge spec when feature is enabled
end end
it 'displays suggest_gitlab_ci_yml popover' do it 'displays suggest_gitlab_ci_yml popover' do
page.find(:css, '.gitlab-ci-yml-selector').click
popover_selector = '.suggest-gitlab-ci-yml' popover_selector = '.suggest-gitlab-ci-yml'
expect(page).to have_css(popover_selector, visible: true) expect(page).to have_css(popover_selector, visible: true)
......
...@@ -8,8 +8,9 @@ RSpec.describe 'Projects > Files > User uses soft wrap while editing file', :js ...@@ -8,8 +8,9 @@ RSpec.describe 'Projects > Files > User uses soft wrap while editing file', :js
user = project.owner user = project.owner
sign_in user sign_in user
visit project_new_blob_path(project, 'master', file_name: 'test_file-name') visit project_new_blob_path(project, 'master', file_name: 'test_file-name')
page.within('.file-editor.code') do page.within('.file-editor.code') do
find('.ace_text-input', visible: false).send_keys 'Touch water with paw then recoil in horror chase dog then find('.inputarea', visible: false).send_keys 'Touch water with paw then recoil in horror chase dog then
run away chase the pig around the house eat owner\'s food, and knock run away chase the pig around the house eat owner\'s food, and knock
dish off table head butt cant eat out of my own dish. Cat is love, cat dish off table head butt cant eat out of my own dish. Cat is love, cat
is life rub face on everything poop on grasses so meow. Playing with is life rub face on everything poop on grasses so meow. Playing with
...@@ -26,17 +27,20 @@ RSpec.describe 'Projects > Files > User uses soft wrap while editing file', :js ...@@ -26,17 +27,20 @@ RSpec.describe 'Projects > Files > User uses soft wrap while editing file', :js
it 'user clicks the "Soft wrap" button and then "No wrap" button' do it 'user clicks the "Soft wrap" button and then "No wrap" button' do
wrapped_content_width = get_content_width wrapped_content_width = get_content_width
toggle_button.click
expect(toggle_button).to have_content 'No wrap'
unwrapped_content_width = get_content_width
expect(unwrapped_content_width).to be < wrapped_content_width
toggle_button.click toggle_button.click do
expect(toggle_button).to have_content 'Soft wrap' expect(toggle_button).to have_content 'Soft wrap'
expect(get_content_width).to be > unwrapped_content_width unwrapped_content_width = get_content_width
expect(unwrapped_content_width).to be > wrapped_content_width
end
toggle_button.click do
expect(toggle_button).to have_content 'No wrap'
expect(get_content_width).to be < unwrapped_content_width
end
end end
def get_content_width def get_content_width
find('.ace_content')[:style].slice!(/width: \d+/).slice!(/\d+/).to_i find('.view-lines', visible: false)[:style].slice!(/width: \d+/).slice!(/\d+/).to_i
end end
end end
...@@ -25,6 +25,6 @@ RSpec.describe 'Projects > Files > User wants to add a .gitignore file' do ...@@ -25,6 +25,6 @@ RSpec.describe 'Projects > Files > User wants to add a .gitignore file' do
expect(page).to have_css('.gitignore-selector .dropdown-toggle-text', text: 'Apply a template') expect(page).to have_css('.gitignore-selector .dropdown-toggle-text', text: 'Apply a template')
expect(page).to have_content('/.bundle') expect(page).to have_content('/.bundle')
expect(page).to have_content('# Gemfile.lock, .ruby-version, .ruby-gemset') expect(page).to have_content('config/initializers/secret_token.rb')
end end
end end
...@@ -67,7 +67,7 @@ RSpec.describe 'Projects > Files > User creates files', :js do ...@@ -67,7 +67,7 @@ RSpec.describe 'Projects > Files > User creates files', :js do
file_name = find('#file_name') file_name = find('#file_name')
file_name.set options[:file_name] || 'README.md' file_name.set options[:file_name] || 'README.md'
find('.ace_text-input', visible: false).send_keys.native.send_keys options[:file_content] || 'Some content' find('.monaco-editor textarea').send_keys.native.send_keys options[:file_content] || 'Some content'
click_button 'Commit changes' click_button 'Commit changes'
end end
...@@ -89,7 +89,7 @@ RSpec.describe 'Projects > Files > User creates files', :js do ...@@ -89,7 +89,7 @@ RSpec.describe 'Projects > Files > User creates files', :js do
it 'creates and commit a new file' do it 'creates and commit a new file' do
find('#editor') find('#editor')
execute_script("ace.edit('editor').setValue('*.rbca')") execute_script("monaco.editor.getModels()[0].setValue('*.rbca')")
fill_in(:file_name, with: 'not_a_file.md') fill_in(:file_name, with: 'not_a_file.md')
fill_in(:commit_message, with: 'New commit message', visible: true) fill_in(:commit_message, with: 'New commit message', visible: true)
click_button('Commit changes') click_button('Commit changes')
...@@ -105,7 +105,7 @@ RSpec.describe 'Projects > Files > User creates files', :js do ...@@ -105,7 +105,7 @@ RSpec.describe 'Projects > Files > User creates files', :js do
it 'creates and commit a new file with new lines at the end of file' do it 'creates and commit a new file with new lines at the end of file' do
find('#editor') find('#editor')
execute_script('ace.edit("editor").setValue("Sample\n\n\n")') execute_script('monaco.editor.getModels()[0].setValue("Sample\n\n\n")')
fill_in(:file_name, with: 'not_a_file.md') fill_in(:file_name, with: 'not_a_file.md')
fill_in(:commit_message, with: 'New commit message', visible: true) fill_in(:commit_message, with: 'New commit message', visible: true)
click_button('Commit changes') click_button('Commit changes')
...@@ -117,7 +117,7 @@ RSpec.describe 'Projects > Files > User creates files', :js do ...@@ -117,7 +117,7 @@ RSpec.describe 'Projects > Files > User creates files', :js do
find('.js-edit-blob').click find('.js-edit-blob').click
find('#editor') find('#editor')
expect(evaluate_script('ace.edit("editor").getValue()')).to eq("Sample\n\n\n") expect(evaluate_script('monaco.editor.getModels()[0].getValue()')).to eq("Sample\n\n\n")
end end
it 'creates and commit a new file with a directory name' do it 'creates and commit a new file with a directory name' do
...@@ -126,7 +126,7 @@ RSpec.describe 'Projects > Files > User creates files', :js do ...@@ -126,7 +126,7 @@ RSpec.describe 'Projects > Files > User creates files', :js do
expect(page).to have_selector('.file-editor') expect(page).to have_selector('.file-editor')
find('#editor') find('#editor')
execute_script("ace.edit('editor').setValue('*.rbca')") execute_script("monaco.editor.getModels()[0].setValue('*.rbca')")
fill_in(:commit_message, with: 'New commit message', visible: true) fill_in(:commit_message, with: 'New commit message', visible: true)
click_button('Commit changes') click_button('Commit changes')
...@@ -141,7 +141,7 @@ RSpec.describe 'Projects > Files > User creates files', :js do ...@@ -141,7 +141,7 @@ RSpec.describe 'Projects > Files > User creates files', :js do
expect(page).to have_selector('.file-editor') expect(page).to have_selector('.file-editor')
find('#editor') find('#editor')
execute_script("ace.edit('editor').setValue('*.rbca')") execute_script("monaco.editor.getModels()[0].setValue('*.rbca')")
fill_in(:file_name, with: 'not_a_file.md') fill_in(:file_name, with: 'not_a_file.md')
fill_in(:commit_message, with: 'New commit message', visible: true) fill_in(:commit_message, with: 'New commit message', visible: true)
fill_in(:branch_name, with: 'new_branch_name', visible: true) fill_in(:branch_name, with: 'new_branch_name', visible: true)
...@@ -176,7 +176,7 @@ RSpec.describe 'Projects > Files > User creates files', :js do ...@@ -176,7 +176,7 @@ RSpec.describe 'Projects > Files > User creates files', :js do
expect(page).to have_selector('.file-editor') expect(page).to have_selector('.file-editor')
find('#editor') find('#editor')
execute_script("ace.edit('editor').setValue('*.rbca')") execute_script("monaco.editor.getModels()[0].setValue('*.rbca')")
fill_in(:file_name, with: 'not_a_file.md') fill_in(:file_name, with: 'not_a_file.md')
fill_in(:commit_message, with: 'New commit message', visible: true) fill_in(:commit_message, with: 'New commit message', visible: true)
......
...@@ -46,9 +46,9 @@ RSpec.describe 'Projects > Files > User edits files', :js do ...@@ -46,9 +46,9 @@ RSpec.describe 'Projects > Files > User edits files', :js do
find('.file-editor', match: :first) find('.file-editor', match: :first)
find('#editor') find('#editor')
execute_script("ace.edit('editor').setValue('*.rbca')") execute_script("monaco.editor.getModels()[0].setValue('*.rbca')")
expect(evaluate_script('ace.edit("editor").getValue()')).to eq('*.rbca') expect(evaluate_script('monaco.editor.getModels()[0].getValue()')).to eq('*.rbca')
end end
it 'does not show the edit link if a file is binary' do it 'does not show the edit link if a file is binary' do
...@@ -67,7 +67,7 @@ RSpec.describe 'Projects > Files > User edits files', :js do ...@@ -67,7 +67,7 @@ RSpec.describe 'Projects > Files > User edits files', :js do
find('.file-editor', match: :first) find('.file-editor', match: :first)
find('#editor') find('#editor')
execute_script("ace.edit('editor').setValue('*.rbca')") execute_script("monaco.editor.getModels()[0].setValue('*.rbca')")
fill_in(:commit_message, with: 'New commit message', visible: true) fill_in(:commit_message, with: 'New commit message', visible: true)
click_button('Commit changes') click_button('Commit changes')
...@@ -85,7 +85,7 @@ RSpec.describe 'Projects > Files > User edits files', :js do ...@@ -85,7 +85,7 @@ RSpec.describe 'Projects > Files > User edits files', :js do
find('.file-editor', match: :first) find('.file-editor', match: :first)
find('#editor') find('#editor')
execute_script("ace.edit('editor').setValue('*.rbca')") execute_script("monaco.editor.getModels()[0].setValue('*.rbca')")
fill_in(:commit_message, with: 'New commit message', visible: true) fill_in(:commit_message, with: 'New commit message', visible: true)
fill_in(:branch_name, with: 'new_branch_name', visible: true) fill_in(:branch_name, with: 'new_branch_name', visible: true)
click_button('Commit changes') click_button('Commit changes')
...@@ -103,7 +103,7 @@ RSpec.describe 'Projects > Files > User edits files', :js do ...@@ -103,7 +103,7 @@ RSpec.describe 'Projects > Files > User edits files', :js do
find('.file-editor', match: :first) find('.file-editor', match: :first)
find('#editor') find('#editor')
execute_script("ace.edit('editor').setValue('*.rbca')") execute_script("monaco.editor.getModels()[0].setValue('*.rbca')")
click_link('Preview changes') click_link('Preview changes')
expect(page).to have_css('.line_holder.new') expect(page).to have_css('.line_holder.new')
...@@ -148,9 +148,9 @@ RSpec.describe 'Projects > Files > User edits files', :js do ...@@ -148,9 +148,9 @@ RSpec.describe 'Projects > Files > User edits files', :js do
find('.file-editor', match: :first) find('.file-editor', match: :first)
find('#editor') find('#editor')
execute_script("ace.edit('editor').setValue('*.rbca')") execute_script("monaco.editor.getModels()[0].setValue('*.rbca')")
expect(evaluate_script('ace.edit("editor").getValue()')).to eq('*.rbca') expect(evaluate_script('monaco.editor.getModels()[0].getValue()')).to eq('*.rbca')
end end
it 'opens the Web IDE in a forked project', :sidekiq_might_not_need_inline do it 'opens the Web IDE in a forked project', :sidekiq_might_not_need_inline do
...@@ -178,7 +178,7 @@ RSpec.describe 'Projects > Files > User edits files', :js do ...@@ -178,7 +178,7 @@ RSpec.describe 'Projects > Files > User edits files', :js do
find('.file-editor', match: :first) find('.file-editor', match: :first)
find('#editor') find('#editor')
execute_script("ace.edit('editor').setValue('*.rbca')") execute_script("monaco.editor.getModels()[0].setValue('*.rbca')")
fill_in(:commit_message, with: 'New commit message', visible: true) fill_in(:commit_message, with: 'New commit message', visible: true)
click_button('Commit changes') click_button('Commit changes')
...@@ -207,7 +207,7 @@ RSpec.describe 'Projects > Files > User edits files', :js do ...@@ -207,7 +207,7 @@ RSpec.describe 'Projects > Files > User edits files', :js do
expect(page).not_to have_button('Cancel') expect(page).not_to have_button('Cancel')
find('#editor') find('#editor')
execute_script("ace.edit('editor').setValue('*.rbca')") execute_script("monaco.editor.getModels()[0].setValue('*.rbca')")
fill_in(:commit_message, with: 'Another commit', visible: true) fill_in(:commit_message, with: 'Another commit', visible: true)
click_button('Commit changes') click_button('Commit changes')
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment