mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-30 04:45:21 -05:00
Add in auto complete
This commit is contained in:
parent
f154f17704
commit
d2ac9a1e9f
7 changed files with 338 additions and 4 deletions
|
@ -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?
|
||||
|
|
|
@ -39,6 +39,14 @@ block content
|
|||
)
|
||||
h4 Settings
|
||||
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"
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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 () ->
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue