Merge "CM6: Add syntax highlighting preference for users without WikiEditor"

This commit is contained in:
jenkins-bot 2024-04-11 14:00:05 +00:00 committed by Gerrit Code Review
commit b97d51ff51
14 changed files with 167 additions and 27 deletions

View file

@ -224,6 +224,15 @@
"codemirror-folded-code"
]
},
"ext.CodeMirror.v6.init": {
"dependencies": [
"ext.CodeMirror.v6",
"ext.CodeMirror.v6.mode.mediawiki"
],
"packageFiles": [
"dist/codemirror.mediawiki.js"
]
},
"ext.CodeMirror.v6.lib": {
"packageFiles": [
"dist/vendor.js"

View file

@ -7,6 +7,9 @@
},
"codemirror-desc": "Provides syntax highlighting in wikitext editor",
"codemirror-toggle-label": "Syntax highlighting",
"codemirror-prefs-summary": "You can learn more about this feature by reading the [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Extension:CodeMirror help page].",
"codemirror-prefs-enable": "Enable syntax highlighting for wikitext",
"codemirror-v6-prefs-colorblind": "Use colorblind-friendly scheme",
"codemirror-prefs-colorblind": "Enable colorblind-friendly scheme for syntax highlighting when editing wikitext",
"codemirror-prefs-colorblind-help": "If you use a gadget for syntax highlighting, this preference will not work.",
"codemirror-find": "Find",
@ -46,5 +49,6 @@
"codemirror-fold-template": "Fold template parameters",
"codemirror-unfold": "unfold",
"codemirror-folded-code": "folded code",
"prefs-accessibility": "Accessibility"
"prefs-accessibility": "Accessibility",
"prefs-syntax-highlighting": "Syntax highlighting"
}

View file

@ -11,7 +11,10 @@
},
"codemirror-desc": "{{desc|name=Code Mirror|url=https://www.mediawiki.org/wiki/Extension:CodeMirror}}\n\nAdditional info: Discription of \"Syntax highlighting\" in wiki\n[[mw:Extension:SyntaxHighlight GeSHi]]",
"codemirror-toggle-label": "Title tooltip for button to toggle CodeMirror in the editing toolbar.",
"codemirror-prefs-summary": "Used in [[Special:Preferences]] in the section titled {{msg-mw|prefs-syntax-highlighting}}, at the top as a summary for the whole section.",
"codemirror-prefs-enable": "Used in user preferences as label for enabling syntax highlighting.",
"codemirror-prefs-colorblind": "Used in user preferences as label for enabling the colorblind-friendly option.",
"codemirror-v6-prefs-colorblind": "Used in user preferences as label for enabling the colorblind-friendly option. This is a shorter version of {{msg-mw|codemirror-prefs-colorblind}} shown under section {{msg-mw|prefs-syntax-highlighting}} on wikis using CodeMirror 6.",
"codemirror-prefs-colorblind-help": "Used in user preferences as remark on the colorblind-friendly option.",
"codemirror-find": "Placeholder text for the input in the CodeMirror search dialog.",
"codemirror-next": "Label for the 'Next' button in the CodeMirror search dialog.",
@ -50,5 +53,6 @@
"codemirror-fold-template": "Tooltip text shown when hovering over a foldable template.",
"codemirror-unfold": "Tooltip text shown when hovering over a placeholder for folded code.",
"codemirror-folded-code": "Aria label for a placeholder for folded code.",
"prefs-accessibility": "Section heading in the user preferences for accessibility topics."
"prefs-accessibility": "Section heading in the user preferences for accessibility topics.",
"prefs-syntax-highlighting": "Used in [[Special:Preferences]], tab \"Editing\" ({{int:prefs-editing}})."
}

View file

@ -55,10 +55,19 @@ class Hooks implements
if ( in_array( 'ext.codeEditor', $out->getModules(), true ) ) {
return false;
}
// Disable CodeMirror when the WikiEditor toolbar is not enabled in preferences
if ( !$this->userOptionsLookup->getOption( $out->getUser(), 'usebetatoolbar' ) ) {
$shouldUseV6 = $this->shouldUseV6( $out );
$useCodeMirror = $this->userOptionsLookup->getBoolOption( $out->getUser(), 'usecodemirror' );
$useWikiEditor = $this->userOptionsLookup->getBoolOption( $out->getUser(), 'usebetatoolbar' );
// Disable CodeMirror 5 when the WikiEditor toolbar is not enabled in preferences.
if ( !$shouldUseV6 && !$useWikiEditor ) {
return false;
}
// In CodeMirror 6, either WikiEditor or the 'usecodemirror' preference must be enabled.
if ( $shouldUseV6 && !$useWikiEditor && !$useCodeMirror ) {
return false;
}
$extensionRegistry = $extensionRegistry ?: ExtensionRegistry::getInstance();
$contentModels = $extensionRegistry->getAttribute( 'CodeMirrorContentModels' );
$isRTL = $out->getTitle()->getPageLanguage()->isRTL();
@ -109,12 +118,18 @@ class Hooks implements
return;
}
$useCodeMirror = $this->userOptionsLookup->getBoolOption( $out->getUser(), 'usecodemirror' );
$useWikiEditor = $this->userOptionsLookup->getBoolOption( $out->getUser(), 'usebetatoolbar' );
if ( $this->shouldUseV6( $out ) ) {
$out->addModules( 'ext.CodeMirror.v6.WikiEditor' );
$out->addModules( $useWikiEditor ?
'ext.CodeMirror.v6.WikiEditor' :
'ext.CodeMirror.v6.init'
);
} else {
$out->addModules( 'ext.CodeMirror.WikiEditor' );
if ( $this->userOptionsLookup->getOption( $out->getUser(), 'usecodemirror' ) ) {
if ( $useCodeMirror ) {
// These modules are predelivered for performance when needed
// keep these modules in sync with ext.CodeMirror.js
$out->addModules( [ 'ext.CodeMirror.lib', 'ext.CodeMirror.mode.mediawiki' ] );
@ -130,7 +145,11 @@ class Hooks implements
*/
public function onEditPage__showReadOnlyForm_initial( $editor, $out ): void {
if ( $this->shouldUseV6( $out ) && $this->shouldLoadCodeMirror( $out ) ) {
$out->addModules( 'ext.CodeMirror.v6.WikiEditor' );
$useWikiEditor = $this->userOptionsLookup->getBoolOption( $out->getUser(), 'usebetatoolbar' );
$out->addModules( $useWikiEditor ?
'ext.CodeMirror.v6.WikiEditor' :
'ext.CodeMirror.v6.init'
);
}
}
@ -167,19 +186,45 @@ class Hooks implements
* @return bool|void True or no return value to continue or false to abort
*/
public function onGetPreferences( $user, &$defaultPreferences ) {
if ( !$this->useV6 ) {
$defaultPreferences['usecodemirror'] = [
'type' => 'api',
];
// The following messages are generated upstream by the 'section' value
// * prefs-accessibility
$defaultPreferences['usecodemirror-colorblind'] = [
'type' => 'toggle',
'label-message' => 'codemirror-prefs-colorblind',
'help-message' => 'codemirror-prefs-colorblind-help',
'section' => 'editing/accessibility',
];
return;
}
// Show message with a link to the Help page under "Syntax highlighting".
// The following messages are generated upstream by the 'section' value:
// * prefs-syntax-highlighting
$defaultPreferences['usecodemirror-summary'] = [
'type' => 'info',
'default' => wfMessage( 'codemirror-prefs-summary' )->parse(),
'raw' => true,
'section' => 'editing/syntax-highlighting'
];
// CodeMirror is disabled by default for all users. It can enabled for everyone
// by default by adding '$wgDefaultUserOptions['usecodemirror'] = 1;' into LocalSettings.php
$defaultPreferences['usecodemirror'] = [
'type' => 'api',
'type' => 'toggle',
'label-message' => 'codemirror-prefs-enable',
'section' => 'editing/syntax-highlighting',
];
// The following messages are generated upstream by the 'section' value
// * prefs-accessibility
$defaultPreferences['usecodemirror-colorblind'] = [
'type' => 'toggle',
'label-message' => 'codemirror-prefs-colorblind',
'help-message' => 'codemirror-prefs-colorblind-help',
'section' => 'editing/accessibility',
'section' => 'editing/syntax-highlighting',
'disable-if' => [ '!==', 'usecodemirror', '1' ]
];
}
}

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1 @@
"use strict";var e=require("ext.CodeMirror.v6"),i=require("ext.CodeMirror.v6.mode.mediawiki");require("ext.CodeMirror.v6.lib");var r=document.getElementById("wpTextbox1"),o=new e(r);o.initialize([o.defaultExtensions,i({bidiIsolation:"rtl"===r.dir})]);

View file

@ -1 +1 @@
"use strict";var e=require("ext.CodeMirror.v6.lib"),t=require("ext.CodeMirror.v6"),r=require("ext.CodeMirror.v6.mode.mediawiki"),i=function(t){function r(t,i){var o;return e._classCallCheck(this,r),(o=e._callSuper(this,r,[t])).langExtension=i,o.useCodeMirror=mw.user.options.get("usecodemirror")>0,o.realtimePreviewHandler=null,o}return e._inherits(r,t),e._createClass(r,[{key:"setCodeMirrorPreference",value:function(t){this.useCodeMirror=t,e._get(e._getPrototypeOf(r.prototype),"setCodeMirrorPreference",this).call(this,t)}},{key:"enableCodeMirror",value:function(){var t=this;if(!this.view){var r=this.$textarea.prop("selectionStart"),i=this.$textarea.prop("selectionEnd"),o=this.$textarea.scrollTop(),a=this.$textarea.is(":focus"),d=[this.defaultExtensions,this.langExtension,e.EditorView.domEventHandlers({blur:function(){return t.$textarea.triggerHandler("blur")},focus:function(){return t.$textarea.triggerHandler("focus")}}),e.EditorView.lineWrapping,e.EditorView.updateListener.of((function(e){e.docChanged&&"function"==typeof t.realtimePreviewHandler&&t.realtimePreviewHandler()}))];if(this.initialize(d),this.addRealtimePreviewHandler(),requestAnimationFrame((function(){t.view.scrollDOM.scrollTop=o})),0!==r||0!==i){var s=e.EditorSelection.range(r,i),n=e.EditorView.scrollIntoView(s);n.value.isSnapshot=!0,this.view.dispatch({selection:e.EditorSelection.create([s]),effects:n})}a&&this.view.focus(),mw.hook("ext.CodeMirror.switch").fire(!0,$(this.view.dom))}}},{key:"addRealtimePreviewHandler",value:function(){var e=this;mw.hook("ext.WikiEditor.realtimepreview.enable").add((function(t){e.realtimePreviewHandler=t.getEventHandler().bind(t)})),mw.hook("ext.WikiEditor.realtimepreview.disable").add((function(){e.realtimePreviewHandler=null}))}},{key:"addCodeMirrorToWikiEditor",value:function(){var e=this,t=this.$textarea.data("wikiEditor-context"),r=t&&t.modules&&t.modules.toolbar;r&&(this.$textarea.wikiEditor("addToToolbar",{section:"main",groups:{codemirror:{tools:{CodeMirror:{label:mw.msg("codemirror-toggle-label"),type:"toggle",oouiIcon:"highlight",action:{type:"callback",execute:function(){return e.switchCodeMirror()}}}}}}}),r.$toolbar.find(".tool[rel=CodeMirror]").attr("id","mw-editbutton-codemirror"),this.readOnly&&this.$textarea.data("wikiEditor-context").$ui.addClass("ext-codemirror-readonly"),this.useCodeMirror&&this.enableCodeMirror(),this.updateToolbarButton(),this.logUsage({editor:"wikitext",enabled:this.useCodeMirror,toggled:!1,edit_start_ts_ms:1e3*parseInt($('input[name="wpStarttime"]').val(),10)||0}))}},{key:"updateToolbarButton",value:function(){var e=$("#mw-editbutton-codemirror");e.toggleClass("mw-editbutton-codemirror-active",this.useCodeMirror),e.data("setActive")&&e.data("setActive")(this.useCodeMirror)}},{key:"switchCodeMirror",value:function(){this.view?(this.setCodeMirrorPreference(!1),this.destroy(),mw.hook("ext.CodeMirror.switch").fire(!1,this.$textarea)):(this.enableCodeMirror(),this.setCodeMirrorPreference(!0)),this.updateToolbarButton(),this.logUsage({editor:"wikitext",enabled:this.useCodeMirror,toggled:!0,edit_start_ts_ms:1e3*parseInt($('input[name="wpStarttime"]').val(),10)||0})}}]),r}(t);mw.loader.getState("ext.wikiEditor")&&mw.hook("wikiEditor.toolbarReady").add((function(e){new i(e,r({bidiIsolation:"rtl"===e.attr("dir")})).addCodeMirrorToWikiEditor()}));
"use strict";var e=require("ext.CodeMirror.v6.lib"),t=require("ext.CodeMirror.v6"),i=require("ext.CodeMirror.v6.mode.mediawiki"),r=function(t){function i(t,r){var o;return e._classCallCheck(this,i),(o=e._callSuper(this,i,[t])).langExtension=r,o.useCodeMirror=mw.user.options.get("usecodemirror")>0,o.realtimePreviewHandler=null,o}return e._inherits(i,t),e._createClass(i,[{key:"setCodeMirrorPreference",value:function(t){this.useCodeMirror=t,e._get(e._getPrototypeOf(i.prototype),"setCodeMirrorPreference",this).call(this,t)}},{key:"enableCodeMirror",value:function(){var t=this;if(!this.view){var i=this.$textarea.prop("selectionStart"),r=this.$textarea.prop("selectionEnd"),o=this.$textarea.scrollTop(),a=this.$textarea.is(":focus"),s=[this.defaultExtensions,this.langExtension,e.EditorView.updateListener.of((function(e){e.docChanged&&"function"==typeof t.realtimePreviewHandler&&t.realtimePreviewHandler()}))];if(this.initialize(s),this.addRealtimePreviewHandler(),requestAnimationFrame((function(){t.view.scrollDOM.scrollTop=o})),0!==i||0!==r){var d=e.EditorSelection.range(i,r),n=e.EditorView.scrollIntoView(d);n.value.isSnapshot=!0,this.view.dispatch({selection:e.EditorSelection.create([d]),effects:n})}a&&this.view.focus(),mw.hook("ext.CodeMirror.switch").fire(!0,$(this.view.dom))}}},{key:"addRealtimePreviewHandler",value:function(){var e=this;mw.hook("ext.WikiEditor.realtimepreview.enable").add((function(t){e.realtimePreviewHandler=t.getEventHandler().bind(t)})),mw.hook("ext.WikiEditor.realtimepreview.disable").add((function(){e.realtimePreviewHandler=null}))}},{key:"addCodeMirrorToWikiEditor",value:function(){var e=this,t=this.$textarea.data("wikiEditor-context"),i=t&&t.modules&&t.modules.toolbar;i&&(this.$textarea.wikiEditor("addToToolbar",{section:"main",groups:{codemirror:{tools:{CodeMirror:{label:mw.msg("codemirror-toggle-label"),type:"toggle",oouiIcon:"highlight",action:{type:"callback",execute:function(){return e.switchCodeMirror()}}}}}}}),i.$toolbar.find(".tool[rel=CodeMirror]").attr("id","mw-editbutton-codemirror"),this.readOnly&&this.$textarea.data("wikiEditor-context").$ui.addClass("ext-codemirror-readonly"),this.useCodeMirror&&this.enableCodeMirror(),this.updateToolbarButton(),this.logUsage({editor:"wikitext",enabled:this.useCodeMirror,toggled:!1,edit_start_ts_ms:1e3*parseInt($('input[name="wpStarttime"]').val(),10)||0}))}},{key:"updateToolbarButton",value:function(){var e=$("#mw-editbutton-codemirror");e.toggleClass("mw-editbutton-codemirror-active",this.useCodeMirror),e.data("setActive")&&e.data("setActive")(this.useCodeMirror)}},{key:"switchCodeMirror",value:function(){this.view?(this.setCodeMirrorPreference(!1),this.destroy(),mw.hook("ext.CodeMirror.switch").fire(!1,this.$textarea)):(this.enableCodeMirror(),this.setCodeMirrorPreference(!0)),this.updateToolbarButton(),this.logUsage({editor:"wikitext",enabled:this.useCodeMirror,toggled:!0,edit_start_ts_ms:1e3*parseInt($('input[name="wpStarttime"]').val(),10)||0})}}]),i}(t);mw.loader.getState("ext.wikiEditor")&&mw.hook("wikiEditor.toolbarReady").add((function(e){new r(e,i({bidiIsolation:"rtl"===e.attr("dir")})).addCodeMirrorToWikiEditor()}));

File diff suppressed because one or more lines are too long

View file

@ -1,3 +1,11 @@
.cm-editor {
border: 1px solid #c8ccd1;
}
// Override for WikiEditor.
.wikiEditor-ui-text .cm-editor {
border: inherit;
}
.cm-matchingBracket,
.cm-focused .cm-matchingBracket {
background-color: #eee;
@ -21,6 +29,10 @@
unicode-bidi: isolate;
}
.ext-codemirror-wrapper {
height: 100%;
}
// Hide all buttons except CodeMirror on read only pages (T301615)
// This is the same hack that CodeEditor uses to customize the toolbar.
// WikiEditor should be updated to better handle read only pages (T188817).

View file

@ -22,6 +22,7 @@ module.exports = [
input: [
'src/codemirror.js',
'src/codemirror.mode.mediawiki.js',
'src/codemirror.mediawiki.js',
'src/codemirror.wikieditor.mediawiki.js'
],

View file

@ -77,6 +77,11 @@ class CodeMirror {
this.heightExtension,
bracketMatching(),
EditorState.readOnly.of( this.readOnly ),
EditorView.domEventHandlers( {
blur: () => this.$textarea.triggerHandler( 'blur' ),
focus: () => this.$textarea.triggerHandler( 'focus' )
} ),
EditorView.lineWrapping,
keymap.of( [
...defaultKeymap,
...searchKeymap
@ -290,10 +295,7 @@ class CodeMirror {
} );
// Add CodeMirror view to the DOM.
this.view = new EditorView( {
state: this.state,
parent: this.$textarea.parent()[ 0 ]
} );
this.#addCodeMirrorToDom();
// Hide native textarea and sync CodeMirror contents upon submission.
this.$textarea.hide();
@ -323,6 +325,22 @@ class CodeMirror {
mw.hook( 'ext.CodeMirror.ready' ).fire( $( this.view.dom ) );
}
/**
* Instantiate the EditorView, adding the CodeMirror editor to the DOM.
* We use a dummy container to ensure that the editor will
* always be placed where the textarea is.
*
* @private
*/
#addCodeMirrorToDom() {
this.$textarea.wrap( '<div class="ext-codemirror-wrapper"></div>' );
this.view = new EditorView( {
state: this.state,
parent: this.$textarea.parent()[ 0 ]
} );
}
/**
* Destroy the CodeMirror instance and revert to the original textarea.
*
@ -335,6 +353,7 @@ class CodeMirror {
const { from, to } = this.view.state.selection.ranges[ 0 ];
$( this.view.dom ).textSelection( 'unregister' );
this.$textarea.textSelection( 'unregister' );
this.$textarea.unwrap( '.ext-codemirror-wrapper' );
this.$textarea.val( this.view.state.doc.toString() );
this.view.destroy();
this.view = null;

View file

@ -0,0 +1,9 @@
import CodeMirror from './codemirror';
import mediaWikiLang from './codemirror.mode.mediawiki';
const textarea = document.getElementById( 'wpTextbox1' );
const cm = new CodeMirror( textarea );
cm.initialize( [
cm.defaultExtensions,
mediaWikiLang( { bidiIsolation: textarea.dir === 'rtl' } )
] );

View file

@ -71,11 +71,6 @@ class CodeMirrorWikiEditor extends CodeMirror {
const extensions = [
this.defaultExtensions,
this.langExtension,
EditorView.domEventHandlers( {
blur: () => this.$textarea.triggerHandler( 'blur' ),
focus: () => this.$textarea.triggerHandler( 'focus' )
} ),
EditorView.lineWrapping,
EditorView.updateListener.of( ( update ) => {
if ( update.docChanged && typeof this.realtimePreviewHandler === 'function' ) {
this.realtimePreviewHandler();

View file

@ -45,7 +45,7 @@ class HookTest extends MediaWikiIntegrationTestCase {
'CodeMirrorV6' => $useCodeMirrorV6,
] );
$userOptionsLookup = $this->createMock( UserOptionsLookup::class );
$userOptionsLookup->method( 'getOption' )->willReturn( true );
$userOptionsLookup->method( 'getBoolOption' )->willReturn( true );
$out = $this->getMockOutputPage();
$out->method( 'getModules' )->willReturn( [] );
@ -87,6 +87,35 @@ class HookTest extends MediaWikiIntegrationTestCase {
self::assertEquals( 'registered', $kinds['usecodemirror'] );
}
/**
* @covers ::onGetPreferences
*/
public function testOnGetPreferencces(): void {
$user = self::getTestUser()->getUser();
$userOptionsLookup = $this->getServiceContainer()->getUserOptionsLookup();
$config = $this->getServiceContainer()->getMainConfig();
// CodeMirror 5
$this->overrideConfigValues( [ 'CodeMirrorV6' => false ] );
$hook = new Hooks( $userOptionsLookup, $config );
$preferences = [];
$hook->onGetPreferences( $user, $preferences );
self::assertArrayHasKey( 'usecodemirror', $preferences );
self::assertArrayHasKey( 'usecodemirror-colorblind', $preferences );
self::assertArrayNotHasKey( 'usecodemirror-summary', $preferences );
self::assertSame( 'api', $preferences['usecodemirror']['type'] );
// CodeMirror 6
$this->overrideConfigValues( [ 'CodeMirrorV6' => true ] );
$hook = new Hooks( $userOptionsLookup, $config );
$preferences = [];
$hook->onGetPreferences( $user, $preferences );
self::assertArrayHasKey( 'usecodemirror', $preferences );
self::assertArrayHasKey( 'usecodemirror-colorblind', $preferences );
self::assertArrayHasKey( 'usecodemirror-summary', $preferences );
self::assertSame( 'toggle', $preferences['usecodemirror']['type'] );
}
/**
* @covers ::shouldLoadCodeMirror
* @dataProvider provideShouldLoadCodeMirror
@ -99,7 +128,9 @@ class HookTest extends MediaWikiIntegrationTestCase {
'gadget' => null,
'contentModel' => CONTENT_MODEL_WIKITEXT,
'useV6' => false,
'isRTL' => false
'isRTL' => false,
'usecodemirror' => true,
'usebetatoolbar' => true,
], $conds );
$this->overrideConfigValues( [
'CodeMirrorV6' => $conds['useV6'],
@ -107,7 +138,11 @@ class HookTest extends MediaWikiIntegrationTestCase {
$out = $this->getMockOutputPage( $conds['contentModel'], $conds['isRTL'] );
$out->method( 'getModules' )->willReturn( $conds['module'] ? [ $conds['module'] ] : [] );
$userOptionsLookup = $this->createMock( UserOptionsLookup::class );
$userOptionsLookup->method( 'getOption' )->willReturn( true );
$userOptionsLookup->method( 'getBoolOption' )
->willReturnMap( [
[ $out->getUser(), 'usecodemirror', 0, $conds['usecodemirror'] ],
[ $out->getUser(), 'usebetatoolbar', 0, $conds['usebetatoolbar'] ]
] );
if ( $conds['gadget'] && !ExtensionRegistry::getInstance()->isLoaded( 'Gadgets' ) ) {
$this->markTestSkipped( 'Skipped as Gadgets extension is not available' );
@ -148,6 +183,12 @@ class HookTest extends MediaWikiIntegrationTestCase {
yield [ [ 'contentModel' => CONTENT_FORMAT_CSS ], false ];
yield [ [ 'isRTL' => true ], false ];
yield [ [ 'isRTL' => true, 'useV6' => true ], true ];
yield [ [ 'usebetatoolbar' => false ], false ];
yield [ [ 'usebetatoolbar' => false, 'useV6' => true ], true ];
yield [ [ 'usebetatoolbar' => false, 'usecodemirror' => false, 'useV6' => true ], false ];
yield [ [ 'usecodemirror' => false ], true ];
yield [ [ 'usecodemirror' => false, 'useV6' => true ], true ];
yield [ [ 'usecodemirror' => false, 'usebetatoolbar' => false, 'useV6' => true ], false ];
}
/**