diff --git a/addons/web/__manifest__.py b/addons/web/__manifest__.py index d375d6ff965..48b396a8946 100644 --- a/addons/web/__manifest__.py +++ b/addons/web/__manifest__.py @@ -95,6 +95,7 @@ This module provides the core of the Odoo Web Client. 'web/static/src/scss/tooltip.scss', 'web/static/src/scss/switch_company_menu.scss', 'web/static/src/scss/debug_manager.scss', + 'web/static/src/scss/ace.scss', 'web/static/src/scss/control_panel.scss', 'web/static/src/scss/fields.scss', 'web/static/src/scss/file_upload.scss', diff --git a/addons/web/static/lib/ace/mode-qweb.js b/addons/web/static/lib/ace/mode-qweb.js new file mode 100644 index 00000000000..d9372fac4cf --- /dev/null +++ b/addons/web/static/lib/ace/mode-qweb.js @@ -0,0 +1,143 @@ +define("ace/mode/qweb_highlight_rules", ["require", "exports", "module", "ace/lib/oop", "ace/mode/xml_highlight_rules"], function (require, exports, module) { + "use strict"; + + var oop = require("../lib/oop"); + var XmlHighlightRules = require("./xml_highlight_rules").XmlHighlightRules; + + var QWebHighlightRules = function () { + XmlHighlightRules.call(this); + const xmlRules = this.$rules; + + const tagRegex = xmlRules.attributes[0].regex; + + this.$rules = Object.assign({}, + xmlRules, + { + attributes: [{ + include: "attributes_odoo", + }, { + include: "attributes_qweb", + }, { + include: "attributes_groups", + }, { + include: "attributes_sample", + }], + + attributes_odoo: [{ + token: ["entity.other.attribute-name.xml.odoo", "keyword.operator.attribute-equals.xml", "text", "string.attribute-value.xml.start", "string.attribute-value.xml.code", "string.attribute-value.xml.end"], + regex: '(domain|attrs|options)(=)(\\s*)(")([^"]*)(")', + }, { + token: ["entity.other.attribute-name.xml.odoo", "keyword.operator.attribute-equals.xml", "text", "string.attribute-value.xml.start", "string.attribute-value.xml.code", "string.attribute-value.xml.end"], + regex: "(domain|attrs|options)(=)(\\s*)(')([^']*)(')", + }], + + attributes_qweb: [{ + token: ["entity.other.attribute-name.xml.qweb", "keyword.operator.attribute-equals.xml", "text", "string.attribute-value.xml.start", "string.attribute-value.xml", "string.attribute-value.xml.end"], + regex: '(t-name|t-call-assets|t-js|t-css)(=)(\\s*)(")([^"]*)(")', + }, { + token: ["entity.other.attribute-name.xml.qweb", "keyword.operator.attribute-equals.xml", "text", "string.attribute-value.xml.start", "string.attribute-value.xml", "string.attribute-value.xml.end"], + regex: "(t-name|t-call-assets|t-js|t-css)(=)(\\s*)(')([^']*)(')", + }, { + token: ["entity.other.attribute-name.xml.qweb", "keyword.operator.attribute-equals.xml", "text", "string.attribute-value.xml.start", "string.attribute-value.xml", "string.attribute-value.xml", "string.attribute-value.xml.code", "string.attribute-value.xml", "string.attribute-value.xml", "string.attribute-value.xml.end"], + regex: '(t-call|t-attf-(?:' + tagRegex + '))(=)(\\s*)(")([^"#{]*)(?:([#{]\\{)([^"}]+)(\\}[}]?))?([^"]*)(")', + }, { + token: ["entity.other.attribute-name.xml.qweb", "keyword.operator.attribute-equals.xml", "text", "string.attribute-value.xml.start", "string.attribute-value.xml", "string.attribute-value.xml", "string.attribute-value.xml.code", "string.attribute-value.xml", "string.attribute-value.xml", "string.attribute-value.xml.end"], + regex: "(t-call|t-attf-(?:" + tagRegex + "))(=)(\\s*)(')([^'#{]*)(?:([#{]\\{)([^'}]+)(\\}[}]?))?([^']*)(')", + }, { + token: ["entity.other.attribute-name.xml.qweb", "keyword.operator.attribute-equals.xml", "text", "string.attribute-value.xml.start", "string.attribute-value.xml.code", "string.attribute-value.xml.end"], + regex: '(t-(?:' + tagRegex + '))(=)(\\s*)(")([^"]*)(")', + }, { + token: ["entity.other.attribute-name.xml.qweb", "keyword.operator.attribute-equals.xml", "text", "string.attribute-value.xml.start", "string.attribute-value.xml.code", "string.attribute-value.xml.end"], + regex: "(t-(?:" + tagRegex + "))(=)(\\s*)(')([^']*)(')", + }], + + attributes_groups: [{ + token: ["entity.other.attribute-name.xml.qweb", "keyword.operator.attribute-equals.xml", "text", "string.attribute-value.xml.start", "string.attribute-value.xml", "string.attribute-value.xml.end"], + regex: '(groups)(=)(\\s*)(")([\\s\\na-zA-Z0-9,_-]*)(")', + }, { + token: ["entity.other.attribute-name.xml.qweb", "keyword.operator.attribute-equals.xml", "text", "string.attribute-value.xml.start", "string.attribute-value.xml", "string.attribute-value.xml.end"], + regex: "(groups)(=)(\\s*)(')((?:[\\s\\na-zA-Z0-9,_-]*)*)(')", + }], + + attributes_sample: [{ + token: "entity.other.attribute-name.xml", + regex: tagRegex, + }, { + token: "keyword.operator.attribute-equals.xml", + regex: "=", + }, { + include: "tag_whitespace", + }, { + include: "attribute_value", + }], + + tag: [{ + token: ["meta.tag.punctuation.tag-open.xml", "meta.tag.punctuation.end-tag-open.xml", "meta.tag.tag-name.xml"], + regex: "(?:(<)|()|(>)", + next: "start", + }, + ], + }], + }, + ); + + if (this.constructor === QWebHighlightRules) { + this.normalizeRules(); + } + }; + + oop.inherits(QWebHighlightRules, XmlHighlightRules); + + exports.QWebHighlightRules = QWebHighlightRules; +}); + +define("ace/mode/qweb",["require","exports","module","ace/lib/oop","ace/lib/lang","ace/mode/text","ace/mode/qweb_highlight_rules","ace/mode/behaviour/xml","ace/mode/folding/xml","ace/worker/worker_client"], function(require, exports, module) { + "use strict"; + + var oop = require("../lib/oop"); + var lang = require("../lib/lang"); + var TextMode = require("./text").Mode; + var QWebHighlightRules = require("./qweb_highlight_rules").QWebHighlightRules; + var XmlBehaviour = require("./behaviour/xml").XmlBehaviour; + var XmlFoldMode = require("./folding/xml").FoldMode; + var WorkerClient = require("../worker/worker_client").WorkerClient; + + var Mode = function() { + this.HighlightRules = QWebHighlightRules; + this.$behaviour = new XmlBehaviour(); + this.foldingRules = new XmlFoldMode(); + }; + + oop.inherits(Mode, TextMode); + + (function() { + + this.voidElements = lang.arrayToMap([]); + + this.blockComment = {start: ""}; + + this.createWorker = function(session) { + var worker = new WorkerClient(["ace"], "ace/mode/xml_worker", "Worker"); + worker.attachToDocument(session.getDocument()); + + worker.on("error", function(e) { + session.setAnnotations(e.data); + }); + + worker.on("terminate", function() { + session.clearAnnotations(); + }); + + return worker; + }; + + this.$id = "ace/mode/xml"; + }).call(Mode.prototype); + + exports.Mode = Mode; +}); diff --git a/addons/web/static/src/js/fields/basic_fields.js b/addons/web/static/src/js/fields/basic_fields.js index 77d52fd61b5..de48b193490 100644 --- a/addons/web/static/src/js/fields/basic_fields.js +++ b/addons/web/static/src/js/fields/basic_fields.js @@ -3520,7 +3520,8 @@ var AceEditor = DebouncedField.extend({ '/web/static/lib/ace/ace.js', [ '/web/static/lib/ace/mode-python.js', - '/web/static/lib/ace/mode-xml.js' + '/web/static/lib/ace/mode-xml.js', + '/web/static/lib/ace/mode-qweb.js' ] ], events: {}, // events are triggered manually for this debounced widget @@ -3613,9 +3614,10 @@ var AceEditor = DebouncedField.extend({ } this.aceEditor.$blockScrolling = true; this.aceSession = this.aceEditor.getSession(); + const mode = this.nodeOptions.mode || 'qweb'; this.aceSession.setOptions({ useWorker: false, - mode: "ace/mode/" + (this.nodeOptions.mode || 'xml'), + mode: "ace/mode/" + (mode === 'xml' ? 'qweb' : mode), tabSize: 2, useSoftTabs: true, }); diff --git a/addons/web/static/src/scss/ace.scss b/addons/web/static/src/scss/ace.scss new file mode 100644 index 00000000000..95adacf494e --- /dev/null +++ b/addons/web/static/src/scss/ace.scss @@ -0,0 +1,15 @@ +.ace_editor > .ace_gutter { + display: block !important; // display even with aria-hidden +} +.ace_editor .ace_qweb { + color: orange !important; +} +.ace_editor .ace_code { + color: fuchsia !important; +} +.ace_editor.ace-monokai .ace_qweb { + color: lightblue !important; +} +.ace_editor.ace-monokai .ace_code { + color: #c94bc9 !important; +} diff --git a/addons/web_editor/static/src/js/common/ace.js b/addons/web_editor/static/src/js/common/ace.js index 0f353f31681..caa219f5103 100644 --- a/addons/web_editor/static/src/js/common/ace.js +++ b/addons/web_editor/static/src/js/common/ace.js @@ -122,6 +122,7 @@ var ViewEditor = Widget.extend({ [ '/web/static/lib/ace/javascript_highlight_rules.js', '/web/static/lib/ace/mode-xml.js', + '/web/static/lib/ace/mode-qweb.js', '/web/static/lib/ace/mode-scss.js', '/web/static/lib/ace/mode-js.js', '/web/static/lib/ace/theme-monokai.js' @@ -349,7 +350,8 @@ var ViewEditor = Widget.extend({ type = type || this.currentType; var editingSession = new window.ace.EditSession(this.resources[type][resID].arch); editingSession.setUseWorker(false); - editingSession.setMode('ace/mode/' + (type || this.currentType)); + let mode = (type || this.currentType); + editingSession.setMode('ace/mode/' + (mode === 'xml' ? 'qweb' : mode)); editingSession.setUndoManager(new window.ace.UndoManager()); editingSession.on('change', function () { _.defer(function () { diff --git a/addons/website/__manifest__.py b/addons/website/__manifest__.py index ea675051beb..07c5248bbe9 100644 --- a/addons/website/__manifest__.py +++ b/addons/website/__manifest__.py @@ -183,6 +183,7 @@ 'website.assets_editor': [ ('include', 'web._assets_helpers'), 'web/static/lib/bootstrap/scss/_variables.scss', + 'web/static/src/scss/ace.scss', 'website/static/src/scss/website.editor.ui.scss', 'website/static/src/scss/website.theme_install.scss', 'website/static/src/js/menu/content.js', diff --git a/addons/website/static/src/js/editor/snippets.options.js b/addons/website/static/src/js/editor/snippets.options.js index 569d2b94891..7647e621fa2 100644 --- a/addons/website/static/src/js/editor/snippets.options.js +++ b/addons/website/static/src/js/editor/snippets.options.js @@ -965,6 +965,7 @@ options.registry.OptionsTab = options.Class.extend({ jsLibs: [ '/web/static/lib/ace/ace.js', '/web/static/lib/ace/mode-xml.js', + '/web/static/lib/ace/mode-qweb.js', ], }); @@ -1163,7 +1164,7 @@ options.registry.OptionsTab = options.Class.extend({ const aceSession = aceEditor.getSession(); aceSession.setOptions({ - mode: "ace/mode/xml", + mode: "ace/mode/qweb", useWorker: false, }); return aceEditor;