diff --git a/app/assets/javascripts/blob_edit/constants.js b/app/assets/javascripts/blob_edit/constants.js new file mode 100644 index 0000000000000000000000000000000000000000..a19da2098cf8a507548781ff689b6b0717287545 --- /dev/null +++ b/app/assets/javascripts/blob_edit/constants.js @@ -0,0 +1,4 @@ +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'); diff --git a/app/assets/javascripts/blob_edit/edit_blob.js b/app/assets/javascripts/blob_edit/edit_blob.js index 011898a5e7ab3a2b77873d69c4534509a3a13fbd..a725b3fe5d615a9466ee476f3275c67583c9fef3 100644 --- a/app/assets/javascripts/blob_edit/edit_blob.js +++ b/app/assets/javascripts/blob_edit/edit_blob.js @@ -3,39 +3,75 @@ import $ from 'jquery'; import axios from '~/lib/utils/axios_utils'; import createFlash from '~/flash'; -import { __ } from '~/locale'; +import { BLOB_EDITOR_ERROR, BLOB_PREVIEW_ERROR } from './constants'; import TemplateSelectorMediator from '../blob/file_template_mediator'; import getModeByFileExtension from '~/lib/utils/ace_utils'; import { addEditorMarkdownListeners } from '~/lib/utils/text_markdown'; +const monacoEnabled = window?.gon?.features?.monacoBlobs; + export default class EditBlob { // The options object has: // assetsPath, filePath, currentAction, projectId, isMarkdown constructor(options) { this.options = options; - this.configureAceEditor(); - this.initModePanesAndLinks(); - this.initSoftWrap(); - this.initFileSelectors(); + const { isMarkdown } = this.options; + Promise.resolve() + .then(() => { + return monacoEnabled ? this.configureMonacoEditor() : this.configureAceEditor(); + }) + .then(() => { + this.initModePanesAndLinks(); + 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() { - const { filePath, assetsPath, isMarkdown } = this.options; + const { filePath, assetsPath } = this.options; ace.config.set('modePath', `${assetsPath}/ace`); ace.config.loadModule('ace/ext/searchbox'); ace.config.loadModule('ace/ext/modelist'); this.editor = ace.edit('editor'); - if (isMarkdown) { - addEditorMarkdownListeners(this.editor); - } - // This prevents warnings re: automatic scrolling being logged this.editor.$blockScrolling = Infinity; - this.editor.focus(); - if (filePath) { this.editor.getSession().setMode(getModeByFileExtension(filePath)); } @@ -81,7 +117,7 @@ export default class EditBlob { currentPane.empty().append(data); currentPane.renderGFM(); }) - .catch(() => createFlash(__('An error occurred previewing the blob'))); + .catch(() => createFlash(BLOB_PREVIEW_ERROR)); } this.$toggleButton.show(); @@ -90,14 +126,19 @@ export default class EditBlob { } initSoftWrap() { - this.isSoftWrapped = false; + this.isSoftWrapped = Boolean(monacoEnabled); this.$toggleButton = $('.soft-wrap-toggle'); + this.$toggleButton.toggleClass('soft-wrap-active', this.isSoftWrapped); this.$toggleButton.on('click', () => this.toggleSoftWrap()); } toggleSoftWrap() { this.isSoftWrapped = !this.isSoftWrapped; this.$toggleButton.toggleClass('soft-wrap-active', this.isSoftWrapped); - this.editor.getSession().setUseWrapMode(this.isSoftWrapped); + if (monacoEnabled) { + this.editor.updateOptions({ wordWrap: this.isSoftWrapped ? 'on' : 'off' }); + } else { + this.editor.getSession().setUseWrapMode(this.isSoftWrapped); + } } } diff --git a/app/assets/javascripts/editor/editor_lite.js b/app/assets/javascripts/editor/editor_lite.js index 020ed6dc867dcd5634e7cd143f15a189f96752ee..6db24a1b7efecf3649b7f2ceb304bf4e372bf4ad 100644 --- a/app/assets/javascripts/editor/editor_lite.js +++ b/app/assets/javascripts/editor/editor_lite.js @@ -1,4 +1,4 @@ -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 languages from '~/ide/lib/languages'; import { defaultEditorOptions } from '~/ide/lib/editor_options'; @@ -70,6 +70,22 @@ export default class Editor { } 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); } } diff --git a/app/views/projects/blob/_editor.html.haml b/app/views/projects/blob/_editor.html.haml index 032df24a6039c1ce345dbc83ecf58db83465525a..67c78620f15640abc4a191198683e0f93151bdb3 100644 --- a/app/views/projects/blob/_editor.html.haml +++ b/app/views/projects/blob/_editor.html.haml @@ -40,7 +40,7 @@ = select_tag :encoding, options_for_select([ "base64", "text" ], "text"), class: 'select2', tabindex: '-1' .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] .js-edit-mode-pane#preview.hide .center diff --git a/app/views/projects/blob/edit.html.haml b/app/views/projects/blob/edit.html.haml index 870e37488cfd56a59f8a69caeb0c5249347261d0..3d84adbc49a213a46308297111cc8022fe4130cd 100644 --- a/app/views/projects/blob/edit.html.haml +++ b/app/views/projects/blob/edit.html.haml @@ -1,7 +1,8 @@ - breadcrumb_title "Repository" - page_title "Edit", @blob.path, @ref -- content_for :page_specific_javascripts do - = page_specific_javascript_tag('lib/ace.js') +- unless Feature.enabled?(:monaco_blobs) + - content_for :page_specific_javascripts do + = page_specific_javascript_tag('lib/ace.js') - if @conflict .alert.alert-danger diff --git a/app/views/projects/blob/new.html.haml b/app/views/projects/blob/new.html.haml index 8f166e9aa16e854bf0af1ca7b6953c499477aab2..f9abcffeeb42c5f7bbcaf9c0d7644b7be16f6e60 100644 --- a/app/views/projects/blob/new.html.haml +++ b/app/views/projects/blob/new.html.haml @@ -1,7 +1,9 @@ - breadcrumb_title "Repository" - page_title "New File", @path.presence, @ref -- content_for :page_specific_javascripts do - = page_specific_javascript_tag('lib/ace.js') +- unless Feature.enabled?(:monaco_blobs) + - content_for :page_specific_javascripts do + = page_specific_javascript_tag('lib/ace.js') + .editor-title-row %h3.page-title.blob-new-page-title New file diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 8fea777c0bfb226bf9ca530cad008fb746daff4b..da7a4038b2c2b0a35b94742fe79e6d4d31d84b95 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -2443,6 +2443,9 @@ msgstr "" msgid "An error occurred while rendering preview broadcast message" msgstr "" +msgid "An error occurred while rendering the editor" +msgstr "" + msgid "An error occurred while reordering issues." msgstr "" diff --git a/spec/features/merge_request/maintainer_edits_fork_spec.rb b/spec/features/merge_request/maintainer_edits_fork_spec.rb index 4db1633abe68aa121af98fbca8eac24b59e078bc..1d5e6e5560143a5e83c8cc3962b715da09e5ae9f 100644 --- a/spec/features/merge_request/maintainer_edits_fork_spec.rb +++ b/spec/features/merge_request/maintainer_edits_fork_spec.rb @@ -37,7 +37,7 @@ end 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' wait_for_requests diff --git a/spec/features/projects/blobs/edit_spec.rb b/spec/features/projects/blobs/edit_spec.rb index 56bf31f24ba5f3ea376fefdb09073741534e79c7..5851121f635782614092257ce1a8e14d983eb3ab 100644 --- a/spec/features/projects/blobs/edit_spec.rb +++ b/spec/features/projects/blobs/edit_spec.rb @@ -36,8 +36,7 @@ def edit_and_commit(commit_changes: true) def fill_editor(content: 'class NextFeature\\nend\\n') wait_for_requests - find('#editor') - execute_script("ace.edit('editor').setValue('#{content}')") + execute_script("monaco.editor.getModels()[0].setValue('#{content}')") end context 'from MR diff' do @@ -67,6 +66,15 @@ def fill_editor(content: 'class NextFeature\\nend\\n') expect(find_by_id('file_path').value).to eq('ci/.gitlab-ci.yml') 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 before do stub_feature_flags(code_navigation: false) diff --git a/spec/features/projects/blobs/user_creates_new_blob_in_new_project_spec.rb b/spec/features/projects/blobs/user_creates_new_blob_in_new_project_spec.rb index f54bceec2b3dfb8a45fe2c01a3c2d3fd75982250..8d107e52db2247ad25d771bfe6b442d261d3dca3 100644 --- a/spec/features/projects/blobs/user_creates_new_blob_in_new_project_spec.rb +++ b/spec/features/projects/blobs/user_creates_new_blob_in_new_project_spec.rb @@ -16,8 +16,7 @@ it 'allows the user to add a new file' do click_link 'New file' - find('#editor') - execute_script('ace.edit("editor").setValue("Hello world")') + execute_script("monaco.editor.getModels()[0].setValue('Hello world')") fill_in(:file_name, with: 'dummy-file') diff --git a/spec/features/projects/blobs/user_follows_pipeline_suggest_nudge_spec.rb b/spec/features/projects/blobs/user_follows_pipeline_suggest_nudge_spec.rb index 5270774b54141f859d644f56c0221abb76ed4d1f..8b43687c71cfd1b0e80d3cb9599107eb19a255c9 100644 --- a/spec/features/projects/blobs/user_follows_pipeline_suggest_nudge_spec.rb +++ b/spec/features/projects/blobs/user_follows_pipeline_suggest_nudge_spec.rb @@ -32,6 +32,8 @@ end it 'displays suggest_gitlab_ci_yml popover' do + page.find(:css, '.gitlab-ci-yml-selector').click + popover_selector = '.suggest-gitlab-ci-yml' expect(page).to have_css(popover_selector, visible: true) diff --git a/spec/features/projects/files/edit_file_soft_wrap_spec.rb b/spec/features/projects/files/edit_file_soft_wrap_spec.rb index ede22204dbdf20bf53893d87589361554ecafd8a..fda024e893dcd7ad51f688b02462fd9f0280a6be 100644 --- a/spec/features/projects/files/edit_file_soft_wrap_spec.rb +++ b/spec/features/projects/files/edit_file_soft_wrap_spec.rb @@ -8,8 +8,9 @@ user = project.owner sign_in user visit project_new_blob_path(project, 'master', file_name: 'test_file-name') + 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 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 @@ -26,17 +27,20 @@ it 'user clicks the "Soft wrap" button and then "No wrap" button' do 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 - expect(toggle_button).to have_content 'Soft wrap' - expect(get_content_width).to be > unwrapped_content_width + + toggle_button.click do + expect(toggle_button).to have_content 'Soft wrap' + 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 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 diff --git a/spec/features/projects/files/gitignore_dropdown_spec.rb b/spec/features/projects/files/gitignore_dropdown_spec.rb index dae1164f7f23bb0c925455c30df25b3fe1b1a8fe..5a39f2bcd984883eda051cd70e8b0b410848087a 100644 --- a/spec/features/projects/files/gitignore_dropdown_spec.rb +++ b/spec/features/projects/files/gitignore_dropdown_spec.rb @@ -25,6 +25,6 @@ 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('# Gemfile.lock, .ruby-version, .ruby-gemset') + expect(page).to have_content('config/initializers/secret_token.rb') end end diff --git a/spec/features/projects/files/user_creates_files_spec.rb b/spec/features/projects/files/user_creates_files_spec.rb index 7d41273011577eb291db58f1a97f1012ad884495..9c86f2dd190e65dbaaea7b34a0311532981306f7 100644 --- a/spec/features/projects/files/user_creates_files_spec.rb +++ b/spec/features/projects/files/user_creates_files_spec.rb @@ -67,7 +67,7 @@ def submit_new_file(options) file_name = find('#file_name') 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' end @@ -89,7 +89,7 @@ def submit_new_file(options) it 'creates and commit a new file' do 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(:commit_message, with: 'New commit message', visible: true) click_button('Commit changes') @@ -105,7 +105,7 @@ def submit_new_file(options) it 'creates and commit a new file with new lines at the end of file' do 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(:commit_message, with: 'New commit message', visible: true) click_button('Commit changes') @@ -117,7 +117,7 @@ def submit_new_file(options) find('.js-edit-blob').click 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 it 'creates and commit a new file with a directory name' do @@ -126,7 +126,7 @@ def submit_new_file(options) expect(page).to have_selector('.file-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) click_button('Commit changes') @@ -141,7 +141,7 @@ def submit_new_file(options) expect(page).to have_selector('.file-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(:commit_message, with: 'New commit message', visible: true) fill_in(:branch_name, with: 'new_branch_name', visible: true) @@ -176,7 +176,7 @@ def submit_new_file(options) expect(page).to have_selector('.file-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(:commit_message, with: 'New commit message', visible: true) diff --git a/spec/features/projects/files/user_edits_files_spec.rb b/spec/features/projects/files/user_edits_files_spec.rb index 1bb931e35ec06b6bf780dff3436e999a503eb310..c8461468c52e38154376d6d85c36221826844837 100644 --- a/spec/features/projects/files/user_edits_files_spec.rb +++ b/spec/features/projects/files/user_edits_files_spec.rb @@ -46,9 +46,9 @@ find('.file-editor', match: :first) 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 it 'does not show the edit link if a file is binary' do @@ -67,7 +67,7 @@ find('.file-editor', match: :first) 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) click_button('Commit changes') @@ -85,7 +85,7 @@ find('.file-editor', match: :first) 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(:branch_name, with: 'new_branch_name', visible: true) click_button('Commit changes') @@ -103,7 +103,7 @@ find('.file-editor', match: :first) find('#editor') - execute_script("ace.edit('editor').setValue('*.rbca')") + execute_script("monaco.editor.getModels()[0].setValue('*.rbca')") click_link('Preview changes') expect(page).to have_css('.line_holder.new') @@ -148,9 +148,9 @@ def expect_fork_status find('.file-editor', match: :first) 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 it 'opens the Web IDE in a forked project', :sidekiq_might_not_need_inline do @@ -178,7 +178,7 @@ def expect_fork_status find('.file-editor', match: :first) 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) click_button('Commit changes') @@ -207,7 +207,7 @@ def expect_fork_status expect(page).not_to have_button('Cancel') 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) click_button('Commit changes')