mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-30 05:05:27 -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?
|
if req.body.fontSize?
|
||||||
user.ace.fontSize = req.body.fontSize
|
user.ace.fontSize = req.body.fontSize
|
||||||
if req.body.autoComplete?
|
if req.body.autoComplete?
|
||||||
user.ace.autoComplete = (req.body.autoComplete == "true")
|
user.ace.autoComplete = req.body.autoComplete
|
||||||
if req.body.spellCheckLanguage?
|
if req.body.spellCheckLanguage?
|
||||||
user.ace.spellCheckLanguage = req.body.spellCheckLanguage
|
user.ace.spellCheckLanguage = req.body.spellCheckLanguage
|
||||||
if req.body.pdfViewer?
|
if req.body.pdfViewer?
|
||||||
|
|
|
@ -38,7 +38,15 @@ block content
|
||||||
ng-cloak
|
ng-cloak
|
||||||
)
|
)
|
||||||
h4 Settings
|
h4 Settings
|
||||||
form
|
form
|
||||||
|
.form-controls
|
||||||
|
label(for="autoComplete") Auto-Complete
|
||||||
|
input.form-control(
|
||||||
|
type="checkbox"
|
||||||
|
name="autoComplete"
|
||||||
|
ng-model="settings.autoComplete"
|
||||||
|
)
|
||||||
|
|
||||||
.form-controls
|
.form-controls
|
||||||
label(for="theme") Theme
|
label(for="theme") Theme
|
||||||
select.form-control(
|
select.form-control(
|
||||||
|
@ -108,6 +116,7 @@ block content
|
||||||
theme="settings.theme",
|
theme="settings.theme",
|
||||||
keybindings="settings.mode",
|
keybindings="settings.mode",
|
||||||
font-size="settings.fontSize",
|
font-size="settings.fontSize",
|
||||||
|
auto-complete="settings.autoComplete",
|
||||||
show-print-margin="false",
|
show-print-margin="false",
|
||||||
sharejs-doc="editor.sharejs_doc",
|
sharejs-doc="editor.sharejs_doc",
|
||||||
last-updated="editor.last_updated"
|
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 [
|
define [
|
||||||
"base"
|
"base"
|
||||||
"ide/editor/undo/UndoManager"
|
|
||||||
"ace/ace"
|
"ace/ace"
|
||||||
|
"ide/editor/undo/UndoManager"
|
||||||
|
"ide/editor/auto-complete/AutoCompleteManager"
|
||||||
"ace/keyboard/vim"
|
"ace/keyboard/vim"
|
||||||
"ace/keyboard/emacs"
|
"ace/keyboard/emacs"
|
||||||
"ace/mode/latex"
|
"ace/mode/latex"
|
||||||
"ace/edit_session"
|
"ace/edit_session"
|
||||||
], (App, UndoManager, Ace) ->
|
], (App, Ace, UndoManager, AutoCompleteManager) ->
|
||||||
LatexMode = require("ace/mode/latex").Mode
|
LatexMode = require("ace/mode/latex").Mode
|
||||||
EditSession = require('ace/edit_session').EditSession
|
EditSession = require('ace/edit_session').EditSession
|
||||||
|
|
||||||
|
@ -17,6 +18,7 @@ define [
|
||||||
showPrintMargin: "="
|
showPrintMargin: "="
|
||||||
keybindings: "="
|
keybindings: "="
|
||||||
fontSize: "="
|
fontSize: "="
|
||||||
|
autoComplete: "="
|
||||||
sharejsDoc: "="
|
sharejsDoc: "="
|
||||||
lastUpdated: "="
|
lastUpdated: "="
|
||||||
}
|
}
|
||||||
|
@ -25,6 +27,8 @@ define [
|
||||||
scope.undo =
|
scope.undo =
|
||||||
show_remote_warning: false
|
show_remote_warning: false
|
||||||
|
|
||||||
|
autoCompleteManager = new AutoCompleteManager(editor)
|
||||||
|
|
||||||
# Prevert Ctrl|Cmd-S from triggering save dialog
|
# Prevert Ctrl|Cmd-S from triggering save dialog
|
||||||
editor.commands.addCommand
|
editor.commands.addCommand
|
||||||
name: "save",
|
name: "save",
|
||||||
|
@ -59,6 +63,14 @@ define [
|
||||||
if sharejs_doc?
|
if sharejs_doc?
|
||||||
attachToAce(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) ->
|
attachToAce = (sharejs_doc) ->
|
||||||
lines = sharejs_doc.getSnapshot().split("\n")
|
lines = sharejs_doc.getSnapshot().split("\n")
|
||||||
editor.setSession(new EditSession(lines))
|
editor.setSession(new EditSession(lines))
|
||||||
|
@ -66,6 +78,8 @@ define [
|
||||||
session.setUseWrapMode(true)
|
session.setUseWrapMode(true)
|
||||||
session.setMode(new LatexMode())
|
session.setMode(new LatexMode())
|
||||||
|
|
||||||
|
autoCompleteManager.bindToSession(session)
|
||||||
|
|
||||||
doc = session.getDocument()
|
doc = session.getDocument()
|
||||||
doc.on "change", () ->
|
doc.on "change", () ->
|
||||||
scope.$apply () ->
|
scope.$apply () ->
|
||||||
|
|
|
@ -18,6 +18,11 @@ define [], () ->
|
||||||
if mode != oldMode
|
if mode != oldMode
|
||||||
@saveSettings({mode: mode})
|
@saveSettings({mode: mode})
|
||||||
|
|
||||||
|
@$scope.$watch "settings.autoComplete", (autoComplete, oldAutoComplete) =>
|
||||||
|
console.log "autoComplete", autoComplete
|
||||||
|
if autoComplete != oldAutoComplete
|
||||||
|
@saveSettings({autoComplete: autoComplete})
|
||||||
|
|
||||||
saveSettings: (data) ->
|
saveSettings: (data) ->
|
||||||
data._csrf = window.csrfToken
|
data._csrf = window.csrfToken
|
||||||
@ide.$http.post "/user/settings", data
|
@ide.$http.post "/user/settings", data
|
||||||
|
|
Loading…
Reference in a new issue