diff --git a/services/web/app/coffee/Features/Project/ProjectController.coffee b/services/web/app/coffee/Features/Project/ProjectController.coffee index 92750a5912..193965c101 100644 --- a/services/web/app/coffee/Features/Project/ProjectController.coffee +++ b/services/web/app/coffee/Features/Project/ProjectController.coffee @@ -275,6 +275,7 @@ module.exports = ProjectController = theme : user.ace.theme fontSize : user.ace.fontSize autoComplete: user.ace.autoComplete + autoPairDelimiters: user.ace.autoPairDelimiters pdfViewer : user.ace.pdfViewer syntaxValidation: user.ace.syntaxValidation } diff --git a/services/web/app/coffee/models/User.coffee b/services/web/app/coffee/models/User.coffee index 099b9ef8e2..fce4b18bc0 100644 --- a/services/web/app/coffee/models/User.coffee +++ b/services/web/app/coffee/models/User.coffee @@ -24,6 +24,7 @@ UserSchema = new Schema theme : {type : String, default: 'textmate'} fontSize : {type : Number, default:'12'} autoComplete: {type : Boolean, default: true} + autoPairDelimiters: {type : Boolean, default: true} spellCheckLanguage : {type : String, default: "en"} pdfViewer : {type : String, default: "pdfjs"} syntaxValidation : {type : Boolean} diff --git a/services/web/app/views/project/editor/editor.pug b/services/web/app/views/project/editor/editor.pug index ebeeee6ceb..8b58c98bcd 100644 --- a/services/web/app/views/project/editor/editor.pug +++ b/services/web/app/views/project/editor/editor.pug @@ -37,6 +37,7 @@ div.full-size( keybindings="settings.mode", font-size="settings.fontSize", auto-complete="settings.autoComplete", + auto-pair-delimiters="settings.autoPairDelimiters", spell-check="!anonymous", spell-check-language="project.spellCheckLanguage" highlights="onlineUserCursorHighlights[editor.open_doc_id]" diff --git a/services/web/app/views/project/editor/left-menu.pug b/services/web/app/views/project/editor/left-menu.pug index 831dc212ae..718aed857d 100644 --- a/services/web/app/views/project/editor/left-menu.pug +++ b/services/web/app/views/project/editor/left-menu.pug @@ -105,6 +105,14 @@ aside#left-menu.full-size( ng-options="o.v as o.n for o in [{ n: 'On', v: true }, { n: 'Off', v: false }]" ) + .form-controls + label(for="autoPairDelimiters") #{translate("auto_pair_delimiters")} + select( + name="autoPairDelimiters" + ng-model="settings.autoPairDelimiters" + ng-options="o.v as o.n for o in [{ n: 'On', v: true }, { n: 'Off', v: false }]" + ) + .form-controls.code-check-setting label(for="syntaxValidation") #{translate("syntax_validation")} select( diff --git a/services/web/public/coffee/ide/editor/directives/aceEditor.coffee b/services/web/public/coffee/ide/editor/directives/aceEditor.coffee index 6c2b81edc4..af633ea369 100644 --- a/services/web/public/coffee/ide/editor/directives/aceEditor.coffee +++ b/services/web/public/coffee/ide/editor/directives/aceEditor.coffee @@ -44,6 +44,7 @@ define [ keybindings: "=" fontSize: "=" autoComplete: "=" + autoPairDelimiters: "=" sharejsDoc: "=" spellCheck: "=" spellCheckLanguage: "=" @@ -78,10 +79,16 @@ define [ editor = ace.edit(element.find(".ace-editor-body")[0]) editor.$blockScrolling = Infinity - # disable auto insertion of brackets and quotes - editor.setOption('behavioursEnabled', false) + # auto-insertion of braces, brackets, dollars + editor.setOption('behavioursEnabled', scope.autoPairDelimiters || false) editor.setOption('wrapBehavioursEnabled', false) + scope.$watch "autoPairDelimiters", (autoPairDelimiters) => + if autoPairDelimiters + editor.setOption('behavioursEnabled', true) + else + editor.setOption('behavioursEnabled', false) + window.editors ||= [] window.editors.push editor diff --git a/services/web/public/coffee/ide/settings/controllers/SettingsController.coffee b/services/web/public/coffee/ide/settings/controllers/SettingsController.coffee index bde479991b..8afb31a9b7 100644 --- a/services/web/public/coffee/ide/settings/controllers/SettingsController.coffee +++ b/services/web/public/coffee/ide/settings/controllers/SettingsController.coffee @@ -24,6 +24,10 @@ define [ if autoComplete != oldAutoComplete settings.saveSettings({autoComplete: autoComplete}) + $scope.$watch "settings.autoPairDelimiters", (autoPairDelimiters, oldAutoPairDelimiters) => + if autoPairDelimiters != oldAutoPairDelimiters + settings.saveSettings({autoPairDelimiters: autoPairDelimiters}) + $scope.$watch "settings.pdfViewer", (pdfViewer, oldPdfViewer) => if pdfViewer != oldPdfViewer settings.saveSettings({pdfViewer: pdfViewer}) @@ -61,4 +65,4 @@ define [ $scope.$apply () => $scope.project.spellCheckLanguage = languageCode delete @ignoreUpdates - ] \ No newline at end of file + ] diff --git a/services/web/public/js/ace-1.2.5/mode-latex.js b/services/web/public/js/ace-1.2.5/mode-latex.js index 8e7bbe4802..4572411661 100644 --- a/services/web/public/js/ace-1.2.5/mode-latex.js +++ b/services/web/public/js/ace-1.2.5/mode-latex.js @@ -195,7 +195,308 @@ oop.inherits(FoldMode, BaseFoldMode); }); -ace.define("ace/mode/latex",["require","exports","module","ace/lib/oop","ace/mode/text","ace/mode/latex_highlight_rules","ace/mode/folding/latex","ace/range","ace/worker/worker_client"], function(require, exports, module) { +ace.define("ace/mode/behaviour/latex",["require","exports","module","ace/lib/oop","ace/mode/behaviour","ace/token_iterator","ace/lib/lang"], function(require, exports, module) { +"use strict"; + +var oop = require("../../lib/oop"); +var Behaviour = require("../behaviour").Behaviour; +var TokenIterator = require("../../token_iterator").TokenIterator; +var lang = require("../../lib/lang"); + +var SAFE_INSERT_IN_TOKENS = + ["text", "paren.rparen", "punctuation.operator"]; +var SAFE_INSERT_BEFORE_TOKENS = + ["text", "paren.rparen", "punctuation.operator", "comment"]; + +var context; +var contextCache = {}; +var initContext = function(editor) { + var id = -1; + if (editor.multiSelect) { + id = editor.selection.index; + if (contextCache.rangeCount != editor.multiSelect.rangeCount) + contextCache = {rangeCount: editor.multiSelect.rangeCount}; + } + if (contextCache[id]) + return context = contextCache[id]; + context = contextCache[id] = { + autoInsertedBrackets: 0, + autoInsertedRow: -1, + autoInsertedLineEnd: "", + maybeInsertedBrackets: 0, + maybeInsertedRow: -1, + maybeInsertedLineStart: "", + maybeInsertedLineEnd: "" + }; +}; + +var getWrapped = function(selection, selected, opening, closing) { + var rowDiff = selection.end.row - selection.start.row; + return { + text: opening + selected + closing, + selection: [ + 0, + selection.start.column + 1, + rowDiff, + selection.end.column + (rowDiff ? 0 : 1) + ] + }; +}; + +var LatexBehaviour = function() { + this.add("braces", "insertion", function(state, action, editor, session, text) { + if (editor.completer && editor.completer.popup && editor.completer.popup.isOpen) { + return; + } + var cursor = editor.getCursorPosition(); + var line = session.doc.getLine(cursor.row); + var lastChar = line[cursor.column-1]; + if (lastChar === '\\') { + return; + } + if (text == '{') { + initContext(editor); + var selection = editor.getSelectionRange(); + var selected = session.doc.getTextRange(selection); + if (selected !== "" && editor.getWrapBehavioursEnabled()) { + return getWrapped(selection, selected, '{', '}'); + } else if (LatexBehaviour.isSaneInsertion(editor, session)) { + LatexBehaviour.recordAutoInsert(editor, session, "}"); + return { + text: '{}', + selection: [1, 1] + }; + } + } else if (text == '}') { + initContext(editor); + var rightChar = line.substring(cursor.column, cursor.column + 1); + if (rightChar == '}') { + var matching = session.$findOpeningBracket('}', {column: cursor.column + 1, row: cursor.row}); + if (matching !== null && LatexBehaviour.isAutoInsertedClosing(cursor, line, text)) { + LatexBehaviour.popAutoInsertedClosing(); + return { + text: '', + selection: [1, 1] + }; + } + } + } + }); + + this.add("braces", "deletion", function(state, action, editor, session, range) { + if (editor.completer && editor.completer.popup && editor.completer.popup.isOpen) { + return; + } + var selected = session.doc.getTextRange(range); + if (!range.isMultiLine() && selected == '{') { + initContext(editor); + var line = session.doc.getLine(range.start.row); + var rightChar = line.substring(range.start.column + 1, range.start.column + 2); + if (rightChar == '}') { + range.end.column++; + return range; + } + } + }); + + this.add("brackets", "insertion", function(state, action, editor, session, text) { + if (editor.completer && editor.completer.popup && editor.completer.popup.isOpen) { + return; + } + var cursor = editor.getCursorPosition(); + var line = session.doc.getLine(cursor.row); + var lastChar = line[cursor.column-1]; + if (lastChar === '\\') { + return; + } + if (text == '[') { + initContext(editor); + var selection = editor.getSelectionRange(); + var selected = session.doc.getTextRange(selection); + if (selected !== "" && editor.getWrapBehavioursEnabled()) { + return getWrapped(selection, selected, '[', ']'); + } else if (LatexBehaviour.isSaneInsertion(editor, session)) { + LatexBehaviour.recordAutoInsert(editor, session, "]"); + return { + text: '[]', + selection: [1, 1] + }; + } + } else if (text == ']') { + initContext(editor); + var rightChar = line.substring(cursor.column, cursor.column + 1); + if (rightChar == ']') { + var matching = session.$findOpeningBracket(']', {column: cursor.column + 1, row: cursor.row}); + if (matching !== null && LatexBehaviour.isAutoInsertedClosing(cursor, line, text)) { + LatexBehaviour.popAutoInsertedClosing(); + return { + text: '', + selection: [1, 1] + }; + } + } + } + }); + + this.add("brackets", "deletion", function(state, action, editor, session, range) { + if (editor.completer && editor.completer.popup && editor.completer.popup.isOpen) { + return; + } + var selected = session.doc.getTextRange(range); + if (!range.isMultiLine() && selected == '[') { + initContext(editor); + var line = session.doc.getLine(range.start.row); + var rightChar = line.substring(range.start.column + 1, range.start.column + 2); + if (rightChar == ']') { + range.end.column++; + return range; + } + } + }); + + this.add("dollars", "insertion", function(state, action, editor, session, text) { + var cursor = editor.getCursorPosition(); + var line = session.doc.getLine(cursor.row); + var lastChar = line[cursor.column-1]; + if (lastChar === '\\') { + return; + } + if (text == '$') { + if (this.lineCommentStart && this.lineCommentStart.indexOf(text) != -1) + return; + initContext(editor); + var quote = text; + var selection = editor.getSelectionRange(); + var selected = session.doc.getTextRange(selection); + if (selected !== "" && selected !== "$" && editor.getWrapBehavioursEnabled()) { + return getWrapped(selection, selected, quote, quote); + } else if (!selected) { + var leftChar = line.substring(cursor.column-1, cursor.column); + var rightChar = line.substring(cursor.column, cursor.column + 1); + + var token = session.getTokenAt(cursor.row, cursor.column); + var rightToken = session.getTokenAt(cursor.row, cursor.column + 1); + + var stringBefore = token && /string|escape/.test(token.type); + var stringAfter = !rightToken || /string|escape/.test(rightToken.type); + + var pair; + if (rightChar == quote) { + pair = stringBefore !== stringAfter; + if (pair && /string\.end/.test(rightToken.type)) + pair = false; + } else { + if (stringBefore && !stringAfter) + return null; // wrap string with different quote + if (stringBefore && stringAfter) + return null; // do not pair quotes inside strings + var wordRe = session.$mode.tokenRe; + wordRe.lastIndex = 0; + var isWordBefore = wordRe.test(leftChar); + wordRe.lastIndex = 0; + var isWordAfter = wordRe.test(leftChar); + if (isWordBefore || isWordAfter) + return null; // before or after alphanumeric + if (rightChar && !/[\s;,.})\]\\]/.test(rightChar)) + return null; // there is rightChar and it isn't closing + pair = true; + } + return { + text: pair ? quote + quote : "", + selection: [1,1] + }; + } + } + }); + + this.add("dollars", "deletion", function(state, action, editor, session, range) { + var selected = session.doc.getTextRange(range); + if (!range.isMultiLine() && (selected == '$')) { + initContext(editor); + var line = session.doc.getLine(range.start.row); + var rightChar = line.substring(range.start.column + 1, range.start.column + 2); + if (rightChar == selected) { + range.end.column++; + return range; + } + } + }); + +}; + + +LatexBehaviour.isSaneInsertion = function(editor, session) { + var cursor = editor.getCursorPosition(); + var iterator = new TokenIterator(session, cursor.row, cursor.column); + if (!this.$matchTokenType(iterator.getCurrentToken() || "text", SAFE_INSERT_IN_TOKENS)) { + var iterator2 = new TokenIterator(session, cursor.row, cursor.column + 1); + if (!this.$matchTokenType(iterator2.getCurrentToken() || "text", SAFE_INSERT_IN_TOKENS)) + return false; + } + iterator.stepForward(); + return iterator.getCurrentTokenRow() !== cursor.row || + this.$matchTokenType(iterator.getCurrentToken() || "text", SAFE_INSERT_BEFORE_TOKENS); +}; + +LatexBehaviour.$matchTokenType = function(token, types) { + return types.indexOf(token.type || token) > -1; +}; + +LatexBehaviour.recordAutoInsert = function(editor, session, bracket) { + var cursor = editor.getCursorPosition(); + var line = session.doc.getLine(cursor.row); + if (!this.isAutoInsertedClosing(cursor, line, context.autoInsertedLineEnd[0])) + context.autoInsertedBrackets = 0; + context.autoInsertedRow = cursor.row; + context.autoInsertedLineEnd = bracket + line.substr(cursor.column); + context.autoInsertedBrackets++; +}; + +LatexBehaviour.recordMaybeInsert = function(editor, session, bracket) { + var cursor = editor.getCursorPosition(); + var line = session.doc.getLine(cursor.row); + if (!this.isMaybeInsertedClosing(cursor, line)) + context.maybeInsertedBrackets = 0; + context.maybeInsertedRow = cursor.row; + context.maybeInsertedLineStart = line.substr(0, cursor.column) + bracket; + context.maybeInsertedLineEnd = line.substr(cursor.column); + context.maybeInsertedBrackets++; +}; + +LatexBehaviour.isAutoInsertedClosing = function(cursor, line, bracket) { + return context.autoInsertedBrackets > 0 && + cursor.row === context.autoInsertedRow && + bracket === context.autoInsertedLineEnd[0] && + line.substr(cursor.column) === context.autoInsertedLineEnd; +}; + +LatexBehaviour.isMaybeInsertedClosing = function(cursor, line) { + return context.maybeInsertedBrackets > 0 && + cursor.row === context.maybeInsertedRow && + line.substr(cursor.column) === context.maybeInsertedLineEnd && + line.substr(0, cursor.column) == context.maybeInsertedLineStart; +}; + +LatexBehaviour.popAutoInsertedClosing = function() { + context.autoInsertedLineEnd = context.autoInsertedLineEnd.substr(1); + context.autoInsertedBrackets--; +}; + +LatexBehaviour.clearMaybeInsertedClosing = function() { + if (context) { + context.maybeInsertedBrackets = 0; + context.maybeInsertedRow = -1; + } +}; + + + +oop.inherits(LatexBehaviour, Behaviour); + +exports.LatexBehaviour = LatexBehaviour; +}); + +ace.define("ace/mode/latex",["require","exports","module","ace/lib/oop","ace/mode/text","ace/mode/latex_highlight_rules","ace/mode/folding/latex","ace/range","ace/worker/worker_client","ace/mode/behaviour/latex"], function(require, exports, module) { "use strict"; var oop = require("../lib/oop"); @@ -204,6 +505,7 @@ var LatexHighlightRules = require("./latex_highlight_rules").LatexHighlightRules var LatexFoldMode = require("./folding/latex").FoldMode; var Range = require("../range").Range; var WorkerClient = require("ace/worker/worker_client").WorkerClient; +var LatexBehaviour = require("./behaviour/latex").LatexBehaviour; var createLatexWorker = function (session) { var doc = session.getDocument(); @@ -361,6 +663,7 @@ var createLatexWorker = function (session) { var Mode = function() { this.HighlightRules = LatexHighlightRules; this.foldingRules = new LatexFoldMode(); + this.$behaviour = new LatexBehaviour(); this.createWorker = createLatexWorker; }; oop.inherits(Mode, TextMode);