diff --git a/services/web/app/coffee/Features/User/UserController.coffee b/services/web/app/coffee/Features/User/UserController.coffee index 89e6f77a4b..62b8d8b203 100644 --- a/services/web/app/coffee/Features/User/UserController.coffee +++ b/services/web/app/coffee/Features/User/UserController.coffee @@ -48,7 +48,7 @@ module.exports = if req.body.fontSize? user.ace.fontSize = req.body.fontSize if req.body.autoComplete? - user.ace.autoComplete = (req.body.autoComplete == "true") + user.ace.autoComplete = req.body.autoComplete if req.body.spellCheckLanguage? user.ace.spellCheckLanguage = req.body.spellCheckLanguage if req.body.pdfViewer? diff --git a/services/web/app/views/project/editor.jade b/services/web/app/views/project/editor.jade index 2f81ef3f75..543176724c 100644 --- a/services/web/app/views/project/editor.jade +++ b/services/web/app/views/project/editor.jade @@ -38,7 +38,15 @@ block content ng-cloak ) h4 Settings - form + form + .form-controls + label(for="autoComplete") Auto-Complete + input.form-control( + type="checkbox" + name="autoComplete" + ng-model="settings.autoComplete" + ) + .form-controls label(for="theme") Theme select.form-control( @@ -108,6 +116,7 @@ block content theme="settings.theme", keybindings="settings.mode", font-size="settings.fontSize", + auto-complete="settings.autoComplete", show-print-margin="false", sharejs-doc="editor.sharejs_doc", last-updated="editor.last_updated" diff --git a/services/web/public/coffee/app/ide/editor/auto-complete/AutoCompleteManager.coffee b/services/web/public/coffee/app/ide/editor/auto-complete/AutoCompleteManager.coffee new file mode 100644 index 0000000000..1a42bedf14 --- /dev/null +++ b/services/web/public/coffee/app/ide/editor/auto-complete/AutoCompleteManager.coffee @@ -0,0 +1,80 @@ +define [ + "ide/editor/auto-complete/SuggestionManager" + "ide/editor/auto-complete/Snippets" + "ace/autocomplete/util" + "ace/autocomplete" + "ace/range" + "ace/ext/language_tools" +], (SuggestionManager, Snippets, Util, AutoComplete) -> + Range = require("ace/range").Range + Autocomplete = AutoComplete.Autocomplete + + Util.retrievePrecedingIdentifier = (text, pos, regex) -> + currentLineOffset = 0 + for i in [(pos-1)..0] + if text[i] == "\n" + currentLineOffset = i + 1 + break + currentLine = text.slice(currentLineOffset, pos) + fragment = getLastCommandFragment(currentLine) or "" + return fragment + + getLastCommandFragment = (lineUpToCursor) -> + if m = lineUpToCursor.match(/(\\[^\\ ]+)$/) + return m[1] + else + return null + + class AutoCompleteManager + constructor: (@editor) -> + @suggestionManager = new SuggestionManager() + + insertMatch = Autocomplete::insertMatch + editor = @editor + Autocomplete::insertMatch = (data) -> + pos = editor.getCursorPosition() + range = new Range(pos.row, pos.column, pos.row, pos.column + 1) + nextChar = editor.session.getTextRange(range) + + # If we are in \begin{it|}, then we need to remove the trailing } + # since it will be adding in with the autocomplete of \begin{item}... + if this.completions.filterText.match(/^\\begin\{/) and nextChar == "}" + editor.session.remove(range) + + insertMatch.call editor.completer, data + + enable: () -> + @editor.setOptions({ + enableBasicAutocompletion: true, + enableSnippets: true + }) + + SnippetCompleter = + getCompletions: (editor, session, pos, prefix, callback) -> + callback null, Snippets + @editor.completers = [@suggestionManager, SnippetCompleter] + + disable: () -> + @editor.setOptions({ + enableBasicAutocompletion: false, + enableSnippets: false + }) + + bindToSession: (@aceSession) -> + @aceSession.on "change", (change) => @onChange(change) + + onChange: (change) -> + cursorPosition = @editor.getCursorPosition() + end = change.data.range.end + # Check that this change was made by us, not a collaborator + # (Cursor is still one place behind) + if end.row == cursorPosition.row and end.column == cursorPosition.column + 1 + if change.data.action == "insertText" + range = new Range(end.row, 0, end.row, end.column) + lineUpToCursor = @aceSession.getTextRange(range) + commandFragment = getLastCommandFragment(lineUpToCursor) + + if commandFragment? and commandFragment.length > 2 + setTimeout () => + @editor.execCommand("startAutocomplete") + , 0 diff --git a/services/web/public/coffee/app/ide/editor/auto-complete/Snippets.coffee b/services/web/public/coffee/app/ide/editor/auto-complete/Snippets.coffee new file mode 100644 index 0000000000..53e0f57fa1 --- /dev/null +++ b/services/web/public/coffee/app/ide/editor/auto-complete/Snippets.coffee @@ -0,0 +1,100 @@ +define () -> + environments = [ + "abstract", + "align", "align*", + "equation", "equation*", + "gather", "gather*", + "multline", "multline*", + "split", + "verbatim" + ] + + snippets = for env in environments + { + caption: "\\begin{#{env}}..." + snippet: """ + \\begin{#{env}} + $1 + \\end{#{env}} + """ + meta: "env" + } + + snippets = snippets.concat [{ + caption: "\\begin{array}..." + snippet: """ + \\begin{array}{${1:cc}} + $2 & $3 \\\\\\\\ + $4 & $5 + \\end{array} + """ + meta: "env" + }, { + caption: "\\begin{figure}..." + snippet: """ + \\begin{figure} + \\centering + \\includegraphics{$1} + \\caption{${2:Caption}} + \\label{${3:fig:my_label}} + \\end{figure} + """ + meta: "env" + }, { + caption: "\\begin{tabular}..." + snippet: """ + \\begin{tabular}{${1:c|c}} + $2 & $3 \\\\\\\\ + $4 & $5 + \\end{tabular} + """ + meta: "env" + }, { + caption: "\\begin{table}..." + snippet: """ + \\begin{table}[$1] + \\centering + \\begin{tabular}{${2:c|c}} + $3 & $4 \\\\\\\\ + $5 & $6 + \\end{tabular} + \\caption{${7:Caption}} + \\label{${8:tab:my_label}} + \\end{table} + """ + meta: "env" + }, { + caption: "\\begin{list}..." + snippet: """ + \\begin{list} + \\item $1 + \\end{list} + """ + meta: "env" + }, { + caption: "\\begin{enumerate}..." + snippet: """ + \\begin{enumerate} + \\item $1 + \\end{enumerate} + """ + meta: "env" + }, { + caption: "\\begin{itemize}..." + snippet: """ + \\begin{itemize} + \\item $1 + \\end{itemize} + """ + meta: "env" + }, { + caption: "\\begin{frame}..." + snippet: """ + \\begin{frame}{${1:Frame Title}} + $2 + \\end{frame} + """ + meta: "env" + }] + + return snippets \ No newline at end of file diff --git a/services/web/public/coffee/app/ide/editor/auto-complete/SuggestionManager.coffee b/services/web/public/coffee/app/ide/editor/auto-complete/SuggestionManager.coffee new file mode 100644 index 0000000000..559a2c5981 --- /dev/null +++ b/services/web/public/coffee/app/ide/editor/auto-complete/SuggestionManager.coffee @@ -0,0 +1,126 @@ +define [], () -> + class Parser + constructor: (@doc) -> + + parse: () -> + commands = [] + seen = {} + while command = @nextCommand() + docState = @doc + + optionalArgs = 0 + while @consumeArgument("[", "]") + optionalArgs++ + + args = 0 + while @consumeArgument("{", "}") + args++ + + commandHash = "#{command}\\#{optionalArgs}\\#{args}" + if !seen[commandHash]? + seen[commandHash] = true + commands.push [command, optionalArgs, args] + + # Reset to before argument to handle nested commands + @doc = docState + + return commands + + # Ignore single letter commands since auto complete is moot then. + commandRegex: /\\([a-zA-Z][a-zA-Z]+)/ + + nextCommand: () -> + i = @doc.search(@commandRegex) + if i == -1 + return false + else + match = @doc.match(@commandRegex)[1] + @doc = @doc.substr(i + match.length + 1) + return match + + consumeWhitespace: () -> + match = @doc.match(/^[ \t\n]*/m)[0] + @doc = @doc.substr(match.length) + + consumeArgument: (openingBracket, closingBracket) -> + @consumeWhitespace() + + if @doc[0] == openingBracket + i = 1 + bracketParity = 1 + while bracketParity > 0 and i < @doc.length + if @doc[i] == openingBracket + bracketParity++ + else if @doc[i] == closingBracket + bracketParity-- + i++ + + if bracketParity == 0 + @doc = @doc.substr(i) + return true + else + return false + else + return false + + class SuggestionManager + getCompletions: (editor, session, pos, prefix, callback) -> + doc = session.getValue() + parser = new Parser(doc) + commands = parser.parse() + + completions = [] + for command in commands + caption = "\\#{command[0]}" + snippet = caption + i = 1 + _.times command[1], () -> + snippet += "[${#{i}}]" + caption += "[]" + i++ + _.times command[2], () -> + snippet += "{${#{i}}}" + caption += "{}" + i++ + unless caption == prefix + completions.push { + caption: caption + snippet: snippet + meta: "cmd" + } + + callback null, completions + + loadCommandsFromDoc: (doc) -> + parser = new Parser(doc) + @commands = parser.parse() + + getSuggestions: (commandFragment) -> + matchingCommands = _.filter @commands, (command) -> + command[0].slice(0, commandFragment.length) == commandFragment + + return _.map matchingCommands, (command) -> + base = "\\" + commandFragment + + args = "" + _.times command[1], () -> args = args + "[]" + _.times command[2], () -> args = args + "{}" + completionBase = command[0].slice(commandFragment.length) + + squareArgsNo = command[1] + curlyArgsNo = command[2] + totalArgs = squareArgsNo + curlyArgsNo + if totalArgs == 0 + completionBeforeCursor = completionBase + completionAfterCurspr = "" + else + completionBeforeCursor = completionBase + args[0] + completionAfterCursor = args.slice(1) + + return { + base: base, + completion: completionBase + args, + completionBeforeCursor: completionBeforeCursor + completionAfterCursor: completionAfterCursor + } + diff --git a/services/web/public/coffee/app/ide/editor/directives/aceEditor.coffee b/services/web/public/coffee/app/ide/editor/directives/aceEditor.coffee index 2fc4c8ee61..ecf78816c0 100644 --- a/services/web/public/coffee/app/ide/editor/directives/aceEditor.coffee +++ b/services/web/public/coffee/app/ide/editor/directives/aceEditor.coffee @@ -1,12 +1,13 @@ define [ "base" - "ide/editor/undo/UndoManager" "ace/ace" + "ide/editor/undo/UndoManager" + "ide/editor/auto-complete/AutoCompleteManager" "ace/keyboard/vim" "ace/keyboard/emacs" "ace/mode/latex" "ace/edit_session" -], (App, UndoManager, Ace) -> +], (App, Ace, UndoManager, AutoCompleteManager) -> LatexMode = require("ace/mode/latex").Mode EditSession = require('ace/edit_session').EditSession @@ -17,6 +18,7 @@ define [ showPrintMargin: "=" keybindings: "=" fontSize: "=" + autoComplete: "=" sharejsDoc: "=" lastUpdated: "=" } @@ -25,6 +27,8 @@ define [ scope.undo = show_remote_warning: false + autoCompleteManager = new AutoCompleteManager(editor) + # Prevert Ctrl|Cmd-S from triggering save dialog editor.commands.addCommand name: "save", @@ -59,6 +63,14 @@ define [ if sharejs_doc? attachToAce(sharejs_doc) + scope.$watch "autoComplete", (autocomplete) -> + if autocomplete + console.log "Enabling auto complete" + autoCompleteManager.enable() + else + console.log "Disabling auto complete" + autoCompleteManager.disable() + attachToAce = (sharejs_doc) -> lines = sharejs_doc.getSnapshot().split("\n") editor.setSession(new EditSession(lines)) @@ -66,6 +78,8 @@ define [ session.setUseWrapMode(true) session.setMode(new LatexMode()) + autoCompleteManager.bindToSession(session) + doc = session.getDocument() doc.on "change", () -> scope.$apply () -> diff --git a/services/web/public/coffee/app/ide/settings/SettingsManager.coffee b/services/web/public/coffee/app/ide/settings/SettingsManager.coffee index e50350a85b..6e9f4abdf9 100644 --- a/services/web/public/coffee/app/ide/settings/SettingsManager.coffee +++ b/services/web/public/coffee/app/ide/settings/SettingsManager.coffee @@ -18,6 +18,11 @@ define [], () -> if mode != oldMode @saveSettings({mode: mode}) + @$scope.$watch "settings.autoComplete", (autoComplete, oldAutoComplete) => + console.log "autoComplete", autoComplete + if autoComplete != oldAutoComplete + @saveSettings({autoComplete: autoComplete}) + saveSettings: (data) -> data._csrf = window.csrfToken @ide.$http.post "/user/settings", data