mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-14 20:40:17 -05:00
Use Ace's built in auto-completer
This commit is contained in:
parent
360fac9510
commit
934f3cbcd8
4 changed files with 60 additions and 251 deletions
|
@ -1,40 +1,47 @@
|
|||
define [
|
||||
"auto-complete/MenuView"
|
||||
"auto-complete/SuggestionManager"
|
||||
"ace/autocomplete/util"
|
||||
"ace/range"
|
||||
], (MenuView, SuggestionManager) ->
|
||||
"ace/ext/language_tools"
|
||||
], (SuggestionManager, Util) ->
|
||||
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
|
||||
constructor: (@ide) ->
|
||||
@aceEditor = @ide.editor.aceEditor
|
||||
@menu = new MenuView()
|
||||
@menu.render(
|
||||
@getAceContentEl().css("font-family"),
|
||||
@getAceContentEl().css("font-size")
|
||||
)
|
||||
@ide.mainAreaManager.getAreaElement("editor").append(@menu.$el)
|
||||
@menu.on "click", (e, suggestion) => @insertSuggestion(suggestion)
|
||||
@menuVisible = false
|
||||
@suggestionManager = new SuggestionManager()
|
||||
|
||||
@aceEditor = @ide.editor.aceEditor
|
||||
@aceEditor.setOptions({
|
||||
enableBasicAutocompletion: true,
|
||||
enableSnippets: true
|
||||
})
|
||||
snippetManager = @aceEditor.completers[0]
|
||||
console.log snippetManager
|
||||
@aceEditor.completers = [snippetManager, @suggestionManager]
|
||||
|
||||
@bindToEditorEvents()
|
||||
@bindToAceInputEvents()
|
||||
|
||||
bindToEditorEvents: () ->
|
||||
@ide.editor.on "change:doc", (@aceSession) =>
|
||||
@refreshSuggestionList()
|
||||
@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) ->
|
||||
@scheduleSuggestionListRefresh()
|
||||
|
||||
cursorPosition = @aceEditor.getCursorPosition()
|
||||
end = change.data.range.end
|
||||
# Check that this change was made by us, not a collaborator
|
||||
|
@ -43,110 +50,9 @@ define [
|
|||
if change.data.action == "insertText"
|
||||
range = new Range(end.row, 0, end.row, end.column)
|
||||
lineUpToCursor = @aceSession.getTextRange(range)
|
||||
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")
|
||||
commandFragment = getLastCommandFragment(lineUpToCursor)
|
||||
|
||||
if commandFragment?
|
||||
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()
|
||||
|
||||
|
|
@ -66,8 +66,32 @@ define [
|
|||
return false
|
||||
|
||||
class SuggestionManager
|
||||
constructor: () ->
|
||||
@commands = []
|
||||
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++
|
||||
|
||||
completions.push {
|
||||
caption: caption
|
||||
snippet: snippet
|
||||
meta: "snippet"
|
||||
}
|
||||
|
||||
callback null, completions
|
||||
|
||||
loadCommandsFromDoc: (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]
|
||||
]
|
Loading…
Reference in a new issue