mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
Merge branch 'master' of github.com:sharelatex/web-sharelatex
This commit is contained in:
commit
1842a65b83
7 changed files with 166 additions and 255 deletions
|
@ -1,40 +1,50 @@
|
||||||
define [
|
define [
|
||||||
"auto-complete/MenuView"
|
|
||||||
"auto-complete/SuggestionManager"
|
"auto-complete/SuggestionManager"
|
||||||
|
"auto-complete/Snippets"
|
||||||
|
"ace/autocomplete/util"
|
||||||
"ace/range"
|
"ace/range"
|
||||||
], (MenuView, SuggestionManager) ->
|
"ace/ext/language_tools"
|
||||||
|
], (SuggestionManager, Snippets, Util) ->
|
||||||
Range = require("ace/range").Range
|
Range = require("ace/range").Range
|
||||||
|
|
||||||
|
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
|
class AutoCompleteManager
|
||||||
constructor: (@ide) ->
|
constructor: (@ide) ->
|
||||||
@aceEditor = @ide.editor.aceEditor
|
@aceEditor = @ide.editor.aceEditor
|
||||||
@menu = new MenuView()
|
@aceEditor.setOptions({
|
||||||
@menu.render(
|
enableBasicAutocompletion: true,
|
||||||
@getAceContentEl().css("font-family"),
|
enableSnippets: true
|
||||||
@getAceContentEl().css("font-size")
|
})
|
||||||
)
|
|
||||||
@ide.mainAreaManager.getAreaElement("editor").append(@menu.$el)
|
SnippetCompleter =
|
||||||
@menu.on "click", (e, suggestion) => @insertSuggestion(suggestion)
|
getCompletions: (editor, session, pos, prefix, callback) ->
|
||||||
@menuVisible = false
|
callback null, Snippets
|
||||||
@suggestionManager = new SuggestionManager()
|
@suggestionManager = new SuggestionManager()
|
||||||
|
|
||||||
|
@aceEditor.completers = [@suggestionManager, SnippetCompleter]
|
||||||
|
|
||||||
@bindToEditorEvents()
|
@bindToEditorEvents()
|
||||||
@bindToAceInputEvents()
|
|
||||||
|
|
||||||
bindToEditorEvents: () ->
|
bindToEditorEvents: () ->
|
||||||
@ide.editor.on "change:doc", (@aceSession) =>
|
@ide.editor.on "change:doc", (@aceSession) =>
|
||||||
@refreshSuggestionList()
|
|
||||||
@aceSession.on "change", (change) => @onChange(change)
|
@aceSession.on "change", (change) => @onChange(change)
|
||||||
@ide.editor.on "scroll", () =>
|
|
||||||
@hideMenu()
|
|
||||||
|
|
||||||
bindToAceInputEvents: () ->
|
|
||||||
@oldOnCommandKey = @aceEditor.keyBinding.onCommandKey
|
|
||||||
@aceEditor.keyBinding.onCommandKey = () => @onKeyPress.apply(@, arguments)
|
|
||||||
$(@aceEditor.renderer.getContainerElement()).on "click", (e) => @onClick(e)
|
|
||||||
|
|
||||||
onChange: (change) ->
|
onChange: (change) ->
|
||||||
@scheduleSuggestionListRefresh()
|
|
||||||
|
|
||||||
cursorPosition = @aceEditor.getCursorPosition()
|
cursorPosition = @aceEditor.getCursorPosition()
|
||||||
end = change.data.range.end
|
end = change.data.range.end
|
||||||
# Check that this change was made by us, not a collaborator
|
# Check that this change was made by us, not a collaborator
|
||||||
|
@ -43,110 +53,9 @@ define [
|
||||||
if change.data.action == "insertText"
|
if change.data.action == "insertText"
|
||||||
range = new Range(end.row, 0, end.row, end.column)
|
range = new Range(end.row, 0, end.row, end.column)
|
||||||
lineUpToCursor = @aceSession.getTextRange(range)
|
lineUpToCursor = @aceSession.getTextRange(range)
|
||||||
commandFragment = @getLastCommandFragment(lineUpToCursor)
|
commandFragment = getLastCommandFragment(lineUpToCursor)
|
||||||
|
|
||||||
if commandFragment
|
|
||||||
suggestions = @suggestionManager.getSuggestions(commandFragment)
|
|
||||||
if suggestions.length > 0
|
|
||||||
@positionMenu(commandFragment.length)
|
|
||||||
@menu.setSuggestions suggestions
|
|
||||||
@showMenu()
|
|
||||||
else
|
|
||||||
@hideMenu()
|
|
||||||
else
|
|
||||||
@hideMenu()
|
|
||||||
else
|
|
||||||
@hideMenu()
|
|
||||||
|
|
||||||
onKeyPress: (e) ->
|
|
||||||
keyCode = e.keyCode
|
|
||||||
|
|
||||||
args = arguments
|
|
||||||
delegate = () =>
|
|
||||||
@oldOnCommandKey.apply(@aceEditor.keyBinding, args)
|
|
||||||
|
|
||||||
if @menuVisible
|
|
||||||
switch keyCode
|
|
||||||
when @keyCodes.UP
|
|
||||||
@menu.moveSelectionUp()
|
|
||||||
when @keyCodes.DOWN
|
|
||||||
@menu.moveSelectionDown()
|
|
||||||
when @keyCodes.ENTER, @keyCodes.TAB
|
|
||||||
@insertSuggestion(@menu.getSelectedSuggestion())
|
|
||||||
e.preventDefault()
|
|
||||||
@hideMenu()
|
|
||||||
when @keyCodes.ESCAPE
|
|
||||||
@hideMenu()
|
|
||||||
else
|
|
||||||
delegate()
|
|
||||||
else
|
|
||||||
delegate()
|
|
||||||
|
|
||||||
positionMenu: (characterOffset) ->
|
|
||||||
characterWidth = @getAceRenderer().characterWidth
|
|
||||||
lineHeight = @getAceRenderer().lineHeight
|
|
||||||
|
|
||||||
pos = @getCursorOffset()
|
|
||||||
pos.top = pos.top + lineHeight
|
|
||||||
styleOffset = 10 # CSS borders and margins
|
|
||||||
pos.left = pos.left - styleOffset - characterOffset * characterWidth
|
|
||||||
|
|
||||||
# We need to position the menu with coordinates relative to the
|
|
||||||
# editor area.
|
|
||||||
editorAreaOffset = @ide.mainAreaManager.getAreaElement("editor").offset()
|
|
||||||
aceOffset = @getAceContentEl().offset()
|
|
||||||
@menu.position
|
|
||||||
top: aceOffset.top - editorAreaOffset.top + pos.top
|
|
||||||
left: aceOffset.left - editorAreaOffset.left + pos.left
|
|
||||||
|
|
||||||
|
|
||||||
insertSuggestion: (suggestion) ->
|
|
||||||
if suggestion?
|
|
||||||
oldCursorPosition = @aceEditor.getCursorPosition()
|
|
||||||
@aceEditor.insert(suggestion.completion)
|
|
||||||
@aceEditor.moveCursorTo(
|
|
||||||
oldCursorPosition.row,
|
|
||||||
oldCursorPosition.column + suggestion.completionBeforeCursor.length
|
|
||||||
)
|
|
||||||
@hideMenu()
|
|
||||||
@aceEditor.focus()
|
|
||||||
|
|
||||||
scheduleSuggestionListRefresh: () ->
|
|
||||||
clearTimeout(@updateTimeoutId) if @updateTimeoutId?
|
|
||||||
@updateTimeoutId = setTimeout((() =>
|
|
||||||
@refreshSuggestionList()
|
|
||||||
delete @updateTimeoutId
|
|
||||||
), 5000)
|
|
||||||
|
|
||||||
refreshSuggestionList: () ->
|
|
||||||
@suggestionManager.loadCommandsFromDoc(@aceSession.doc.getAllLines().join("\n"))
|
|
||||||
|
|
||||||
onClick: () ->
|
|
||||||
@hideMenu()
|
|
||||||
|
|
||||||
getLastCommandFragment: (line) ->
|
|
||||||
if m = line.match(/\\([^\\ ]+)$/)
|
|
||||||
m[1]
|
|
||||||
else
|
|
||||||
null
|
|
||||||
|
|
||||||
showMenu: () ->
|
|
||||||
@menu.show()
|
|
||||||
@menuVisible = true
|
|
||||||
|
|
||||||
hideMenu: () ->
|
|
||||||
@menu.hide()
|
|
||||||
@menuVisible = false
|
|
||||||
|
|
||||||
keyCodes: "UP": 38, "DOWN": 40, "ENTER": 13, "TAB": 9, "ESCAPE": 27
|
|
||||||
|
|
||||||
getCursorOffset: () ->
|
|
||||||
# This is fragile and relies on the internal Ace API not changing.
|
|
||||||
# See $moveTextAreaToCursor in
|
|
||||||
# https://github.com/ajaxorg/ace/blob/master/lib/ace/virtual_renderer.js
|
|
||||||
@aceEditor.renderer.$cursorLayer.$pixelPos
|
|
||||||
|
|
||||||
getAceRenderer: () -> @aceEditor.renderer
|
|
||||||
|
|
||||||
getAceContentEl: () -> $(@aceEditor.renderer.getContainerElement()).find(".ace_content")
|
|
||||||
|
|
||||||
|
if commandFragment? and commandFragment.length > 2
|
||||||
|
setTimeout () =>
|
||||||
|
@aceEditor.execCommand("startAutocomplete")
|
||||||
|
, 0
|
||||||
|
|
|
@ -1,57 +0,0 @@
|
||||||
define [
|
|
||||||
"libs/backbone"
|
|
||||||
"libs/mustache"
|
|
||||||
], () ->
|
|
||||||
MenuView = Backbone.View.extend
|
|
||||||
tagName: "ul"
|
|
||||||
className: "auto-complete-menu"
|
|
||||||
|
|
||||||
templates:
|
|
||||||
suggestion: $("#autoCompleteSuggestionTemplate").html()
|
|
||||||
|
|
||||||
render: (fontFamily, fontSize) ->
|
|
||||||
@$el.css
|
|
||||||
position: "absolute"
|
|
||||||
"font-family": fontFamily
|
|
||||||
"font-size": fontSize
|
|
||||||
return @$el
|
|
||||||
|
|
||||||
setSuggestions: (suggestions) ->
|
|
||||||
@$el.children().off()
|
|
||||||
@$el.empty()
|
|
||||||
@suggestions = []
|
|
||||||
for suggestion in suggestions
|
|
||||||
do (suggestion) =>
|
|
||||||
el = $(Mustache.to_html(@templates.suggestion, suggestion))
|
|
||||||
@$el.append(el)
|
|
||||||
el.on "click", (e) => @trigger("click", e, suggestion)
|
|
||||||
@suggestions.push suggestion: suggestion, el: el
|
|
||||||
@selectSuggestionAtIndex 0
|
|
||||||
|
|
||||||
selectSuggestionAtIndex: (index) ->
|
|
||||||
if index >= 0 and index < @suggestions.length
|
|
||||||
@$("li").removeClass "selected"
|
|
||||||
@suggestions[index].el.addClass "selected"
|
|
||||||
@selectedIndex = index
|
|
||||||
|
|
||||||
moveSelectionDown: () ->
|
|
||||||
if @selectedIndex? and @selectedIndex < @suggestions.length - 1
|
|
||||||
@selectSuggestionAtIndex @selectedIndex + 1
|
|
||||||
|
|
||||||
moveSelectionUp: () ->
|
|
||||||
if @selectedIndex? and @selectedIndex > 0
|
|
||||||
@selectSuggestionAtIndex @selectedIndex - 1
|
|
||||||
|
|
||||||
getSelectedSuggestion: () ->
|
|
||||||
if @selectedIndex? and @suggestions[@selectedIndex]?
|
|
||||||
@suggestions[@selectedIndex].suggestion
|
|
||||||
|
|
||||||
position: (pos) ->
|
|
||||||
@$el.css
|
|
||||||
top: pos.top
|
|
||||||
left: pos.left
|
|
||||||
|
|
||||||
show: () -> @$el.show()
|
|
||||||
hide: () -> @$el.hide()
|
|
||||||
|
|
||||||
|
|
92
services/web/public/coffee/auto-complete/Snippets.coffee
Normal file
92
services/web/public/coffee/auto-complete/Snippets.coffee
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
define () ->
|
||||||
|
environments = [
|
||||||
|
"abstract",
|
||||||
|
"align", "align*",
|
||||||
|
"equation", "equation*",
|
||||||
|
"gather", "gather*",
|
||||||
|
"mutliline", "multiline*",
|
||||||
|
"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{frame}..."
|
||||||
|
snippet: """
|
||||||
|
\\begin{frame}{${1:Frame Title}}
|
||||||
|
$2
|
||||||
|
\\end{frame}
|
||||||
|
"""
|
||||||
|
meta: "env"
|
||||||
|
}]
|
||||||
|
|
||||||
|
return snippets
|
|
@ -1,6 +1,4 @@
|
||||||
define [
|
define [], () ->
|
||||||
"auto-complete/commands"
|
|
||||||
], (commands) ->
|
|
||||||
class Parser
|
class Parser
|
||||||
constructor: (@doc) ->
|
constructor: (@doc) ->
|
||||||
|
|
||||||
|
@ -66,8 +64,32 @@ define [
|
||||||
return false
|
return false
|
||||||
|
|
||||||
class SuggestionManager
|
class SuggestionManager
|
||||||
constructor: () ->
|
getCompletions: (editor, session, pos, prefix, callback) ->
|
||||||
@commands = []
|
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) ->
|
loadCommandsFromDoc: (doc) ->
|
||||||
parser = new Parser(doc)
|
parser = new Parser(doc)
|
||||||
|
|
|
@ -1,64 +0,0 @@
|
||||||
define () -> [
|
|
||||||
# [<command>, <square brackets args>, <curly bracket args>]
|
|
||||||
# E.g. ["includegraphics", 1 ,1] => \includegraphics[]{}
|
|
||||||
|
|
||||||
# Common
|
|
||||||
["emph", 0, 1]
|
|
||||||
|
|
||||||
# Greek letters
|
|
||||||
["alpha", 0, 0]
|
|
||||||
["beta", 0, 0]
|
|
||||||
["gamma", 0, 0]
|
|
||||||
["delta", 0, 0]
|
|
||||||
["eta", 0, 0]
|
|
||||||
["theta", 0, 0]
|
|
||||||
["iota", 0, 0]
|
|
||||||
["kappa", 0, 0]
|
|
||||||
["lambda", 0, 0]
|
|
||||||
["phi", 0, 0]
|
|
||||||
["psi", 0, 0]
|
|
||||||
["mu", 0, 0]
|
|
||||||
["nu", 0, 0]
|
|
||||||
["chi", 0, 0]
|
|
||||||
["xsi", 0, 0]
|
|
||||||
["upsilon", 0, 0]
|
|
||||||
["Lambda", 0, 0]
|
|
||||||
["Omega", 0, 0]
|
|
||||||
["Gamma", 0, 0]
|
|
||||||
["Delta", 0, 0]
|
|
||||||
|
|
||||||
# Maths
|
|
||||||
["infty", 0, 0]
|
|
||||||
["frac", 0, 2]
|
|
||||||
["int", 0, 0]
|
|
||||||
["sum", 0, 0]
|
|
||||||
["sin", 0, 0]
|
|
||||||
["cos", 0, 0]
|
|
||||||
|
|
||||||
# LaTeX commands
|
|
||||||
["begin", 0, 1]
|
|
||||||
["end", 0, 1]
|
|
||||||
["includegraphics", 0, 1]
|
|
||||||
["includegraphics", 1, 1]
|
|
||||||
["section", 0, 1]
|
|
||||||
["chapter", 0, 1]
|
|
||||||
["subsection", 0, 1]
|
|
||||||
["subsubsection", 0, 1]
|
|
||||||
["part", 0, 1]
|
|
||||||
["author", 0, 1]
|
|
||||||
["title", 0, 1]
|
|
||||||
["documentclass", 0, 1]
|
|
||||||
["documentclass", 1, 1]
|
|
||||||
["usepackage", 0, 1]
|
|
||||||
["usepackage", 1, 1]
|
|
||||||
|
|
||||||
# Font commands
|
|
||||||
["textit", 0, 1]
|
|
||||||
["textrm", 0, 1]
|
|
||||||
["textsf", 0, 1]
|
|
||||||
["texttt", 0, 1]
|
|
||||||
|
|
||||||
["newcommand", 0, 2]
|
|
||||||
["renewcommand", 0, 2]
|
|
||||||
["newenvironment", 0, 3]
|
|
||||||
]
|
|
|
@ -125,12 +125,10 @@ define [
|
||||||
|
|
||||||
mode = window.userSettings.mode
|
mode = window.userSettings.mode
|
||||||
theme = window.userSettings.theme
|
theme = window.userSettings.theme
|
||||||
fontSize = window.userSettings.fontSize
|
|
||||||
|
|
||||||
chosenKeyBindings = keybindings[mode]
|
chosenKeyBindings = keybindings[mode]
|
||||||
aceEditor.setKeyboardHandler(chosenKeyBindings)
|
aceEditor.setKeyboardHandler(chosenKeyBindings)
|
||||||
aceEditor.setTheme("ace/theme/#{window.userSettings.theme}")
|
aceEditor.setTheme("ace/theme/#{window.userSettings.theme}")
|
||||||
document.getElementById('editor').style.fontSize = fontSize+'px'
|
|
||||||
aceEditor.setShowPrintMargin(false)
|
aceEditor.setShowPrintMargin(false)
|
||||||
|
|
||||||
# Prevert Ctrl|Cmd-S from triggering save dialog
|
# Prevert Ctrl|Cmd-S from triggering save dialog
|
||||||
|
|
|
@ -17,6 +17,8 @@ define [
|
||||||
|
|
||||||
new DropboxSettingsManager @ide
|
new DropboxSettingsManager @ide
|
||||||
|
|
||||||
|
@setFontSize()
|
||||||
|
|
||||||
if @ide?
|
if @ide?
|
||||||
@ide.on "afterJoinProject", (project) =>
|
@ide.on "afterJoinProject", (project) =>
|
||||||
@project = project
|
@project = project
|
||||||
|
@ -102,6 +104,15 @@ define [
|
||||||
$confirm.off 'click'
|
$confirm.off 'click'
|
||||||
$modal.find('.cancel').click (e)->
|
$modal.find('.cancel').click (e)->
|
||||||
$modal.modal('hide')
|
$modal.modal('hide')
|
||||||
|
|
||||||
|
setFontSize: () ->
|
||||||
|
@fontSizeCss = $("<style/>")
|
||||||
|
@fontSizeCss.text """
|
||||||
|
.ace_editor, .ace_content {
|
||||||
|
font-size: #{window.userSettings.fontSize}px;
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
$(document.body).append(@fontSizeCss)
|
||||||
|
|
||||||
bindToProjectName: () ->
|
bindToProjectName: () ->
|
||||||
@project.on "change:name", (project, newName) ->
|
@project.on "change:name", (project, newName) ->
|
||||||
|
|
Loading…
Reference in a new issue