src/main/scala/com/github/gtache/lsp/editor/EditorEventManager.scala
/**
* Copyright 2017-2018 Guillaume Tâche
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.github.gtache.lsp.editor
import java.awt._
import java.awt.event.{KeyEvent, MouseAdapter, MouseEvent}
import java.io.File
import java.net.URI
import java.util.concurrent.{ExecutionException, TimeUnit, TimeoutException}
import java.util.{Collections, Timer, TimerTask}
import com.github.gtache.lsp.client.languageserver.ServerOptions
import com.github.gtache.lsp.client.languageserver.requestmanager.RequestManager
import com.github.gtache.lsp.client.languageserver.wrapper.LanguageServerWrapperImpl
import com.github.gtache.lsp.contributors.psi.LSPPsiElement
import com.github.gtache.lsp.contributors.rename.LSPRenameProcessor
import com.github.gtache.lsp.requests.{HoverHandler, Timeouts, WorkspaceEditHandler}
import com.github.gtache.lsp.utils.{DocumentUtils, FileUtils, GUIUtils}
import com.intellij.codeInsight.CodeInsightSettings
import com.intellij.codeInsight.completion.InsertionContext
import com.intellij.codeInsight.hint.HintManager
import com.intellij.codeInsight.lookup._
import com.intellij.lang.LanguageDocumentation
import com.intellij.openapi.command.CommandProcessor
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.editor.colors.EditorColors
import com.intellij.openapi.editor.event._
import com.intellij.openapi.editor.ex.EditorSettingsExternalizable
import com.intellij.openapi.editor.markup._
import com.intellij.openapi.editor.{Editor, LogicalPosition, ScrollType}
import com.intellij.openapi.fileEditor.{FileDocumentManager, FileEditorManager, OpenFileDescriptor, TextEditor}
import com.intellij.openapi.fileTypes.PlainTextLanguage
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.TextRange
import com.intellij.openapi.util.text.StringUtil
import com.intellij.openapi.vfs.{LocalFileSystem, VirtualFile}
import com.intellij.psi.{PsiDocumentManager, PsiElement}
import com.intellij.ui.Hint
import com.intellij.uiDesigner.core.{GridConstraints, GridLayoutManager, Spacer}
import javax.swing.{JFrame, JLabel, JPanel}
import org.eclipse.lsp4j._
import org.eclipse.lsp4j.jsonrpc.JsonRpcException
import scala.collection.mutable
object EditorEventManager {
private val HOVER_TIME_THRES: Long = EditorSettingsExternalizable.getInstance().getQuickDocOnMouseOverElementDelayMillis * 1000000
private val SCHEDULE_THRES = 10000000 //Time before the Timer is scheduled
private val POPUP_THRES = HOVER_TIME_THRES / 1000000 + 20
private val CTRL_THRES = 500000000 //Time between requests when ctrl is pressed (500ms)
private val uriToManager: mutable.Map[String, EditorEventManager] = mutable.HashMap()
private val editorToManager: mutable.Map[Editor, EditorEventManager] = mutable.HashMap()
@volatile private var isKeyPressed = false
@volatile private var isCtrlDown = false
@volatile private var ctrlRange: CtrlRangeMarker = _
KeyboardFocusManager.getCurrentKeyboardFocusManager.addKeyEventDispatcher((e: KeyEvent) => this.synchronized {
e.getID match {
case KeyEvent.KEY_PRESSED =>
isKeyPressed = true
if (e.getKeyCode == KeyEvent.VK_CONTROL) isCtrlDown = true
case KeyEvent.KEY_RELEASED =>
isKeyPressed = false
if (e.getKeyCode == KeyEvent.VK_CONTROL) {
isCtrlDown = false
if (ctrlRange != null) ctrlRange.dispose()
ctrlRange = null
}
case _ =>
}
false
})
/**
* @param uri A file uri
* @return The manager for the given uri, or None
*/
def forUri(uri: String): Option[EditorEventManager] = {
prune()
uriToManager.get(uri)
}
/**
* @param editor An editor
* @return The manager for the given editor, or None
*/
def forEditor(editor: Editor): Option[EditorEventManager] = {
prune()
editorToManager.get(editor)
}
/**
* Tells the server that all the documents will be saved
*/
def willSaveAll(): Unit = {
prune()
editorToManager.foreach(e => e._2.willSave())
}
private def prune(): Unit = {
editorToManager.filter(e => !e._2.wrapper.isActive).keys.foreach(editorToManager.remove)
uriToManager.filter(e => !e._2.wrapper.isActive).keys.foreach(uriToManager.remove)
}
}
/**
* Class handling events related to an Editor (a Document)
*
* @param editor The "watched" editor
* @param mouseListener A listener for mouse clicks
* @param mouseMotionListener A listener for mouse movement
* @param documentListener A listener for keystrokes
* @param selectionListener A listener for selection changes in the editor
* @param requestManager The related RequestManager, connected to the right LanguageServer
* @param serverOptions the options of the server regarding completion, signatureHelp, syncKind, etc
* @param wrapper The corresponding LanguageServerWrapper
*/
class EditorEventManager(val editor: Editor, val mouseListener: EditorMouseListener, val mouseMotionListener: EditorMouseMotionListener,
val documentListener: DocumentListener, val selectionListener: SelectionListener,
val requestManager: RequestManager, val serverOptions: ServerOptions, val wrapper: LanguageServerWrapperImpl) {
import EditorEventManager._
import GUIUtils.createAndShowEditorHint
import com.github.gtache.lsp.requests.Timeout._
import com.github.gtache.lsp.utils.ApplicationUtils._
import scala.collection.JavaConverters._
private val identifier: TextDocumentIdentifier = new TextDocumentIdentifier(FileUtils.editorToURIString(editor))
private val LOG: Logger = Logger.getInstance(classOf[EditorEventManager])
private val changesParams = new DidChangeTextDocumentParams(new VersionedTextDocumentIdentifier(), Collections.singletonList(new TextDocumentContentChangeEvent()))
private val selectedSymbHighlights: mutable.Set[RangeHighlighter] = mutable.HashSet()
private val diagnosticsHighlights: mutable.Set[DiagnosticRangeHighlighter] = mutable.HashSet()
private val syncKind = serverOptions.syncKind
private val completionTriggers =
if (serverOptions.completionOptions != null && serverOptions.completionOptions.getTriggerCharacters != null)
serverOptions.completionOptions.getTriggerCharacters.asScala.toSet.filter(s => s != ".")
else Set[String]()
private val signatureTriggers =
if (serverOptions.signatureHelpOptions != null && serverOptions.signatureHelpOptions.getTriggerCharacters != null)
serverOptions.signatureHelpOptions.getTriggerCharacters.asScala.toSet
else Set[String]()
private val documentOnTypeFormattingOptions = serverOptions.documentOnTypeFormattingOptions
private val onTypeFormattingTriggers =
if (documentOnTypeFormattingOptions != null && documentOnTypeFormattingOptions.getMoreTriggerCharacter != null)
(documentOnTypeFormattingOptions.getMoreTriggerCharacter.asScala += documentOnTypeFormattingOptions.getFirstTriggerCharacter).toSet
else if (documentOnTypeFormattingOptions != null)
Set(documentOnTypeFormattingOptions.getFirstTriggerCharacter)
else
Set[String]()
private val project: Project = editor.getProject
@volatile var needSave = false
private var hoverThread = new Timer("Hover", true)
private var version: Int = -1
private var predTime: Long = -1L
private var ctrlTime: Long = -1L
private var isOpen: Boolean = false
private var mouseInEditor: Boolean = true
private var currentHint: Hint = _
uriToManager.put(FileUtils.editorToURIString(editor), this)
editorToManager.put(editor, this)
changesParams.getTextDocument.setUri(identifier.getUri)
/**
* Calls onTypeFormatting or signatureHelp if the character typed was a trigger character
*
* @param c The character just typed
*/
def characterTyped(c: Char): Unit = {
if (completionTriggers.contains(c.toString)) {
//completion(DocumentUtils.offsetToLSPPos(editor,editor.getCaretModel.getCurrentCaret.getOffset))
} else if (signatureTriggers.contains(c.toString)) {
signatureHelp()
} else if (onTypeFormattingTriggers.contains(c.toString)) {
onTypeFormatting(c.toString)
}
}
/**
* Calls signatureHelp at the current editor caret position
*/
def signatureHelp(): Unit = {
val lPos = editor.getCaretModel.getCurrentCaret.getLogicalPosition
val point = editor.logicalPositionToXY(lPos)
val params = new TextDocumentPositionParams(identifier, DocumentUtils.logicalToLSPPos(lPos, editor))
pool(() => {
if (!editor.isDisposed) {
val future = requestManager.signatureHelp(params)
if (future != null) {
try {
val signature = future.get(SIGNATURE_TIMEOUT, TimeUnit.MILLISECONDS)
wrapper.notifySuccess(Timeouts.SIGNATURE)
if (signature != null) {
val signatures = signature.getSignatures
if (signatures != null && !signatures.isEmpty) {
val scalaSignatures = signatures.asScala
val activeSignatureIndex = signature.getActiveSignature
val activeParameterIndex = signature.getActiveParameter
val activeParameter = scalaSignatures(activeSignatureIndex).getParameters.get(activeParameterIndex).getLabel
val builder = StringBuilder.newBuilder
builder.append("<html>")
scalaSignatures.take(activeSignatureIndex).foreach(sig => builder.append(sig.getLabel).append("<br>"))
builder.append("<b>").append(scalaSignatures(activeSignatureIndex).getLabel
.replace(activeParameter, "<font color=\"yellow\">" + activeParameter + "</font>")).append("</b>")
scalaSignatures.drop(activeSignatureIndex + 1).foreach(sig => builder.append("<br>").append(sig.getLabel))
builder.append("</html>")
invokeLater(() => currentHint = createAndShowEditorHint(editor, builder.toString(), point, HintManager.UNDER, HintManager.HIDE_BY_OTHER_HINT))
}
}
} catch {
case e: TimeoutException =>
LOG.warn(e)
wrapper.notifyFailure(Timeouts.SIGNATURE)
case e@(_: java.io.IOException | _: JsonRpcException | _: ExecutionException) =>
LOG.warn(e)
wrapper.crashed(e.asInstanceOf[Exception])
}
}
}
})
}
/**
* Formats the document when a trigger character is typed
*
* @param c The trigger character
*/
private def onTypeFormatting(c: String): Unit = {
pool(() => {
if (!editor.isDisposed) {
val params = new DocumentOnTypeFormattingParams()
params.setCh(c)
params.setPosition(DocumentUtils.logicalToLSPPos(editor.getCaretModel.getCurrentCaret.getLogicalPosition, editor))
params.setTextDocument(identifier)
params.setOptions(new FormattingOptions())
val future = requestManager.onTypeFormatting(params)
if (future != null) {
try {
val edits = future.get(FORMATTING_TIMEOUT, TimeUnit.MILLISECONDS)
wrapper.notifySuccess(Timeouts.FORMATTING)
if (edits != null) invokeLater(() => applyEdit(edits = edits.asScala, name = "On type formatting"))
} catch {
case e: TimeoutException =>
LOG.warn(e)
wrapper.notifyFailure(Timeouts.FORMATTING)
case e@(_: java.io.IOException | _: JsonRpcException | _: ExecutionException) =>
LOG.warn(e)
wrapper.crashed(e.asInstanceOf[Exception])
}
}
}
})
}
/**
* Applies the given edits to the document
*
* @param version The version of the edits (will be discarded if older than current version)
* @param edits The edits to apply
* @param name The name of the edits (Rename, for example)
* @param closeAfter will close the file after edits if set to true
* @return True if the edits were applied, false otherwise
*/
def applyEdit(version: Int = Int.MaxValue, edits: Iterable[TextEdit], name: String = "Apply LSP edits", closeAfter: Boolean = false): Boolean = {
val runnable = getEditsRunnable(version, edits, name)
writeAction(() => {
if (runnable != null) CommandProcessor.getInstance().executeCommand(project, runnable, name, "LSPPlugin", editor.getDocument)
if (closeAfter) {
FileEditorManager.getInstance(project)
.closeFile(PsiDocumentManager.getInstance(project).getPsiFile(editor.getDocument).getVirtualFile)
}
})
if (runnable != null) true else false
}
/**
* Returns a Runnable used to apply the given edits and save the document
* Used by WorkspaceEditHandler (allows to revert a rename for example)
*
* @param version The edit version
* @param edits The edits
* @param name The name of the edit
* @return The runnable
*/
def getEditsRunnable(version: Int = Int.MaxValue, edits: Iterable[TextEdit], name: String = "Apply LSP edits"): Runnable = {
if (version >= this.version) {
val document = editor.getDocument
if (document.isWritable) {
() => {
edits.foreach(edit => {
val text = edit.getNewText
val range = edit.getRange
val start = DocumentUtils.LSPPosToOffset(editor, range.getStart)
val end = DocumentUtils.LSPPosToOffset(editor, range.getEnd)
if (text == "" || text == null) {
document.deleteString(start, end)
} else if (end - start <= 0) {
document.insertString(start, text)
} else {
document.replaceString(start, end, text)
}
})
saveDocument()
}
} else {
LOG.warn("Document is not writable")
null
}
} else {
LOG.warn("Edit version " + version + " is older than current version " + this.version)
null
}
}
private def saveDocument(): Unit = {
invokeLater(() => writeAction(() => FileDocumentManager.getInstance().saveDocument(editor.getDocument)))
}
/**
* Retrieves the commands needed to apply a CodeAction
*
* @param element The element which needs the CodeAction
* @return The list of commands, or null if none are given / the request times out
*/
def codeAction(element: LSPPsiElement): Iterable[Command] = {
val params = new CodeActionParams()
params.setTextDocument(identifier)
val range = new Range(DocumentUtils.offsetToLSPPos(editor, element.start), DocumentUtils.offsetToLSPPos(editor, element.end))
params.setRange(range)
val context = new CodeActionContext(diagnosticsHighlights.map(_.diagnostic).toList.asJava)
params.setContext(context)
val future = requestManager.codeAction(params)
if (future != null) {
try {
val res = future.get(CODEACTION_TIMEOUT, TimeUnit.MILLISECONDS).asScala
wrapper.notifySuccess(Timeouts.CODEACTION)
res
} catch {
case e: TimeoutException =>
LOG.warn(e)
wrapper.notifyFailure(Timeouts.CODEACTION)
null
case e@(_: java.io.IOException | _: JsonRpcException | _: ExecutionException) =>
LOG.warn(e)
wrapper.crashed(e.asInstanceOf[Exception])
null
}
} else {
null
}
}
/**
* Returns the completion suggestions given a position
*
* @param pos The LSP position
* @return The suggestions
*/
def completion(pos: Position): Iterable[_ <: LookupElement] = {
val request = requestManager.completion(new CompletionParams(identifier, pos))
if (request != null) {
try {
val res = request.get(COMPLETION_TIMEOUT, TimeUnit.MILLISECONDS)
wrapper.notifySuccess(Timeouts.COMPLETION)
if (res != null) {
import scala.collection.JavaConverters._
val completion /*: CompletionList | List[CompletionItem] */ = if (res.isLeft) res.getLeft.asScala else res.getRight
/**
* Creates a LookupElement given a CompletionItem
*
* @param item The CompletionItem
* @return The corresponding LookupElement
*/
def createLookupItem(item: CompletionItem): LookupElement = {
val addTextEdits = item.getAdditionalTextEdits
val command = item.getCommand
val data = item.getData
val detail = item.getDetail
val doc = item.getDocumentation
val filterText = item.getFilterText
val insertText = item.getInsertText
val insertFormat = item.getInsertTextFormat
val kind = item.getKind
val label = item.getLabel
val textEdit = item.getTextEdit
val sortText = item.getSortText
val presentableText = if (label != null && label != "") label else if (insertText != null) insertText else ""
val tailText = if (detail != null) detail else ""
val iconProvider = GUIUtils.getIconProviderFor(wrapper.getServerDefinition)
val icon = iconProvider.getCompletionIcon(kind)
var lookupElementBuilder: LookupElementBuilder = null
/* .withRenderer((element: LookupElement, presentation: LookupElementPresentation) => { //TODO later
presentation match {
case realPresentation: RealLookupElementPresentation =>
if (!realPresentation.hasEnoughSpaceFor(presentation.getItemText, presentation.isItemTextBold)) {
}
}
})*/
if (textEdit != null) {
if (addTextEdits != null) {
lookupElementBuilder = LookupElementBuilder.create("")
.withInsertHandler((context: InsertionContext, item: LookupElement) => {
context.commitDocument()
invokeLater(() => {
applyEdit(edits = addTextEdits.asScala :+ textEdit, name = "Completion : " + label)
if (command != null) executeCommands(Iterable(command))
})
})
} else {
lookupElementBuilder = LookupElementBuilder.create("")
.withInsertHandler((context: InsertionContext, item: LookupElement) => {
context.commitDocument()
invokeLater(() => {
applyEdit(edits = Seq(textEdit), name = "Completion : " + label)
if (command != null) executeCommands(Iterable(command))
})
})
}
} else if (addTextEdits != null) {
lookupElementBuilder = LookupElementBuilder.create("")
.withInsertHandler((context: InsertionContext, item: LookupElement) => {
context.commitDocument()
invokeLater(() => {
applyEdit(edits = addTextEdits.asScala, name = "Completion : " + label)
if (command != null) executeCommands(Iterable(command))
})
})
} else {
lookupElementBuilder = LookupElementBuilder.create(if (insertText != null && insertText != "") insertText else label)
if (command != null) lookupElementBuilder = lookupElementBuilder.withInsertHandler((context: InsertionContext, item: LookupElement) => {
context.commitDocument()
invokeLater(() => {
executeCommands(Iterable(command))
})
})
}
if (kind == CompletionItemKind.Keyword) lookupElementBuilder = lookupElementBuilder.withBoldness(true)
lookupElementBuilder.withPresentableText(presentableText).withTailText(tailText, true).withIcon(icon).withAutoCompletionPolicy(AutoCompletionPolicy.SETTINGS_DEPENDENT)
}
completion match {
case c: CompletionList =>
c.getItems.asScala.map(item => {
createLookupItem(item)
})
case l: Iterable[CompletionItem@unchecked] => l.map(item => {
createLookupItem(item)
})
}
} else Iterable()
}
catch {
case e: TimeoutException =>
LOG.warn(e)
wrapper.notifyFailure(Timeouts.COMPLETION)
Iterable.empty
case e@(_: java.io.IOException | _: JsonRpcException | _: ExecutionException) =>
LOG.warn(e)
wrapper.crashed(e.asInstanceOf[Exception])
Iterable.empty
}
} else Iterable.empty
}
/**
* Sends commands to execute to the server and applies the changes returned if the future returns a WorkspaceEdit
*
* @param commands The commands to execute
*/
def executeCommands(commands: Iterable[Command]): Unit = {
pool(() => {
if (!editor.isDisposed) {
commands.map(c => {
val params = new ExecuteCommandParams()
params.setArguments(c.getArguments)
params.setCommand(c.getCommand)
requestManager.executeCommand(params)
}).foreach(f => {
if (f != null) {
try {
val ret = f.get(EXECUTE_COMMAND_TIMEOUT, TimeUnit.MILLISECONDS)
wrapper.notifySuccess(Timeouts.EXECUTE_COMMAND)
ret match {
case e: WorkspaceEdit => WorkspaceEditHandler.applyEdit(e, name = "Execute command")
case _ =>
}
} catch {
case e: TimeoutException =>
LOG.warn(e)
wrapper.notifyFailure(Timeouts.EXECUTE_COMMAND)
case e@(_: java.io.IOException | _: JsonRpcException | _: ExecutionException) =>
LOG.warn(e)
wrapper.crashed(e.asInstanceOf[Exception])
}
}
})
}
})
}
/**
* Applies the diagnostics to the document
*
* @param diagnostics The diagnostics to apply from the server
*/
def diagnostics(diagnostics: Iterable[Diagnostic]): Unit = {
pool(() => {
if (!editor.isDisposed) {
invokeLater(() => {
diagnosticsHighlights.foreach(highlight => editor.getMarkupModel.removeHighlighter(highlight.rangeHighlighter))
diagnosticsHighlights.clear()
})
for (diagnostic <- diagnostics) {
val code = diagnostic.getCode
val message = diagnostic.getMessage
val source = diagnostic.getSource
val range = diagnostic.getRange
val severity = diagnostic.getSeverity
LOG.info("code : " + code + " message : " + message + " source : " + source + " range : " + range)
val (start, end) = (DocumentUtils.LSPPosToOffset(editor, range.getStart), DocumentUtils.LSPPosToOffset(editor, range.getEnd))
val markupModel = editor.getMarkupModel
val colorScheme = editor.getColorsScheme
val (effectType, effectColor, layer) = severity match {
case null => null
case DiagnosticSeverity.Error => (EffectType.WAVE_UNDERSCORE, java.awt.Color.RED, HighlighterLayer.ERROR)
case DiagnosticSeverity.Warning => (EffectType.WAVE_UNDERSCORE, java.awt.Color.YELLOW, HighlighterLayer.WARNING)
case DiagnosticSeverity.Information => (EffectType.WAVE_UNDERSCORE, java.awt.Color.GRAY, HighlighterLayer.WARNING)
case DiagnosticSeverity.Hint => (EffectType.BOLD_DOTTED_LINE, java.awt.Color.GRAY, HighlighterLayer.WARNING)
}
invokeLater(() => {
if (!editor.isDisposed) {
diagnosticsHighlights
.add(DiagnosticRangeHighlighter(markupModel.addRangeHighlighter(start, end, layer,
new TextAttributes(colorScheme.getDefaultForeground, colorScheme.getDefaultBackground, effectColor, effectType, Font.PLAIN), HighlighterTargetArea.EXACT_RANGE),
diagnostic))
}
})
}
}
})
}
/**
* Handles the DocumentChanged events
*
* @param event The DocumentEvent
*/
def documentChanged(event: DocumentEvent): Unit = {
if (!editor.isDisposed) {
if (event.getDocument == editor.getDocument) {
predTime = System.nanoTime() //So that there are no hover events while typing
changesParams.getTextDocument.setVersion({
version += 1
version - 1
})
syncKind match {
case TextDocumentSyncKind.None =>
case TextDocumentSyncKind.Incremental =>
val changeEvent = changesParams.getContentChanges.get(0)
val newText = event.getNewFragment
val offset = event.getOffset
val newTextLength = event.getNewLength
val lspPosition: Position = DocumentUtils.offsetToLSPPos(editor, offset)
val startLine = lspPosition.getLine
val startColumn = lspPosition.getCharacter
val oldText = event.getOldFragment
//if text was deleted/replaced, calculate the end position of inserted/deleted text
val (endLine, endColumn) = if (oldText.length() > 0) {
val line = startLine + StringUtil.countNewLines(oldText)
val oldLines = oldText.toString.split('\n')
val oldTextLength = if (oldLines.isEmpty) 0 else oldLines.last.length
val column = if (oldLines.length == 1) startColumn + oldTextLength else oldTextLength
(line, column)
} else (startLine, startColumn) //if insert or no text change, the end position is the same
val range = new Range(new Position(startLine, startColumn), new Position(endLine, endColumn))
changeEvent.setRange(range)
changeEvent.setRangeLength(newTextLength)
changeEvent.setText(newText.toString)
case TextDocumentSyncKind.Full =>
changesParams.getContentChanges.get(0).setText(editor.getDocument.getText())
}
requestManager.didChange(changesParams)
} else {
LOG.error("Wrong document for the EditorEventManager")
}
}
}
/**
* Notifies the server that the corresponding document has been closed
*/
def documentClosed(): Unit = {
pool(() => {
if (isOpen) {
requestManager.didClose(new DidCloseTextDocumentParams(identifier))
isOpen = false
editorToManager.remove(editor)
uriToManager.remove(FileUtils.editorToURIString(editor))
} else {
LOG.warn("Editor " + identifier.getUri + " was already closed")
}
})
}
def documentOpened(): Unit = {
pool(() => {
if (!editor.isDisposed) {
if (isOpen) {
LOG.warn("Editor " + editor + " was already open")
} else {
requestManager.didOpen(new DidOpenTextDocumentParams(new TextDocumentItem(identifier.getUri, wrapper.serverDefinition.id, {
version += 1
version - 1
}, editor.getDocument.getText)))
isOpen = true
}
}
})
}
/**
* Notifies the server that the corresponding document has been saved
*/
def documentSaved(): Unit = {
pool(() => {
if (!editor.isDisposed) {
val params: DidSaveTextDocumentParams = new DidSaveTextDocumentParams(identifier, editor.getDocument.getText)
requestManager.didSave(params)
}
})
}
/**
* Indicates that the document will be saved
*/
//TODO Manual
def willSave(): Unit = {
if (wrapper.isWillSaveWaitUntil && !needSave) willSaveWaitUntil() else pool(() => {
if (!editor.isDisposed) requestManager.willSave(new WillSaveTextDocumentParams(identifier, TextDocumentSaveReason.Manual))
})
}
/**
* If the server supports willSaveWaitUntil, the LSPVetoer will check if a save is needed
* (needSave will basically alterate between true or false, so the document will always be saved)
*/
private def willSaveWaitUntil(): Unit = {
if (wrapper.isWillSaveWaitUntil) {
pool(() => {
if (!editor.isDisposed) {
val params = new WillSaveTextDocumentParams(identifier, TextDocumentSaveReason.Manual)
val future = requestManager.willSaveWaitUntil(params)
if (future != null) {
try {
val edits = future.get(WILLSAVE_TIMEOUT, TimeUnit.MILLISECONDS)
wrapper.notifySuccess(Timeouts.WILLSAVE)
if (edits != null) {
invokeLater(() => applyEdit(edits = edits.asScala, name = "WaitUntil edits"))
}
} catch {
case e: TimeoutException =>
LOG.warn(e)
wrapper.notifyFailure(Timeouts.WILLSAVE)
case e@(_: java.io.IOException | _: JsonRpcException | _: ExecutionException) =>
LOG.warn(e)
wrapper.crashed(e.asInstanceOf[Exception])
} finally {
needSave = true
saveDocument()
}
} else {
needSave = true
saveDocument()
}
}
})
} else {
LOG.error("Server doesn't support WillSaveWaitUntil")
needSave = true
saveDocument()
}
}
/**
* Gets references, synchronously
*
* @param offset The offset of the element
* @return A list of start/end offset
*/
def documentReferences(offset: Int): Iterable[(Int, Int)] = {
val params = new ReferenceParams()
val context = new ReferenceContext()
context.setIncludeDeclaration(true)
params.setContext(context)
params.setPosition(DocumentUtils.offsetToLSPPos(editor, offset))
params.setTextDocument(identifier)
val future = requestManager.references(params)
if (future != null) {
try {
val references = future.get(REFERENCES_TIMEOUT, TimeUnit.MILLISECONDS)
wrapper.notifySuccess(Timeouts.REFERENCES)
if (references != null) {
references.asScala.collect {
case l: Location if l.getUri == identifier.getUri =>
(DocumentUtils.LSPPosToOffset(editor, l.getRange.getStart), DocumentUtils.LSPPosToOffset(editor, l.getRange.getEnd))
}
} else {
null
}
} catch {
case e: TimeoutException =>
LOG.warn(e)
wrapper.notifyFailure(Timeouts.REFERENCES)
null
case e@(_: java.io.IOException | _: JsonRpcException | _: ExecutionException) =>
LOG.warn(e)
wrapper.crashed(e.asInstanceOf[Exception])
null
}
}
else {
null
}
}
/**
* @return The current diagnostics highlights
*/
def getDiagnostics: mutable.Set[DiagnosticRangeHighlighter] = {
diagnosticsHighlights.clone()
}
def getElementAtOffset(offset: Int): LSPPsiElement = {
computableReadAction(() => {
if (!editor.isDisposed) {
val params = new TextDocumentPositionParams(identifier, DocumentUtils.offsetToLSPPos(editor, offset))
val future = requestManager.documentHighlight(params)
if (future != null) {
try {
val res = future.get(DOC_HIGHLIGHT_TIMEOUT, TimeUnit.MILLISECONDS)
wrapper.notifySuccess(Timeouts.DOC_HIGHLIGHT)
if (res != null && !editor.isDisposed)
res.asScala.map(dh => new TextRange(DocumentUtils.LSPPosToOffset(editor, dh.getRange.getStart), DocumentUtils.LSPPosToOffset(editor, dh.getRange.getEnd)))
.find(range => range.getStartOffset <= offset && offset <= range.getEndOffset)
.map(range => LSPPsiElement(editor.getDocument.getText(range), project, range.getStartOffset, range.getEndOffset, PsiDocumentManager.getInstance(project).getPsiFile(editor.getDocument)))
.orNull
else null
} catch {
case e: TimeoutException =>
wrapper.notifyFailure(Timeouts.DOC_HIGHLIGHT)
LOG.warn(e)
null
case e@(_: java.io.IOException | _: JsonRpcException | _: ExecutionException) =>
LOG.warn(e)
wrapper.crashed(e.asInstanceOf[Exception])
null
}
} else null
}
else null
})
}
/**
* Called when the mouse is clicked
* At the moment, is used by CTRL+click to see references / goto definition
*
* @param e The mouse event
*/
def mouseClicked(e: EditorMouseEvent): Unit = {
if (isCtrlDown) {
if (ctrlRange == null)
createCtrlRange(DocumentUtils.logicalToLSPPos(editor.xyToLogicalPosition(e.getMouseEvent.getPoint), editor), null)
if (ctrlRange != null) {
val loc = ctrlRange.loc
invokeLater(() => {
if (!editor.isDisposed) {
val offset = editor.logicalPositionToOffset(editor.xyToLogicalPosition(e.getMouseEvent.getPoint))
if (identifier.getUri == loc.getUri && DocumentUtils.LSPPosToOffset(editor, loc.getRange.getStart) <= offset && offset <= DocumentUtils.LSPPosToOffset(editor, loc.getRange.getEnd)) {
showReferences(offset)
} else {
val file = LocalFileSystem.getInstance().findFileByIoFile(new File(new URI(FileUtils.sanitizeURI(loc.getUri))))
val descriptor = new OpenFileDescriptor(project, file)
writeAction(() => {
val newEditor = FileEditorManager.getInstance(project).openTextEditor(descriptor, true)
val startOffset = DocumentUtils.LSPPosToOffset(newEditor, loc.getRange.getStart)
newEditor.getCaretModel.getCurrentCaret.moveToOffset(startOffset)
newEditor.getScrollingModel.scrollToCaret(ScrollType.CENTER)
})
}
if (ctrlRange != null) ctrlRange.dispose()
ctrlRange = null
}
})
}
}
}
/**
* Queries references and show them in a window, given the offset of the symbol in the editor
*
* @param offset The offset
*/
def showReferences(offset: Int): Unit = {
invokeLater(() => {
if (!editor.isDisposed) {
writeAction(() => editor.getCaretModel.getCurrentCaret.moveToOffset(offset))
showReferences(includeDefinition = false)
}
})
}
/**
* Queries references and show a window with these references (click on a row to get to the location)
*/
def showReferences(includeDefinition: Boolean = true): Unit = {
pool(() => {
if (!editor.isDisposed) {
val context = new ReferenceContext(includeDefinition)
val params = new ReferenceParams(context)
params.setTextDocument(identifier)
val serverPos = computableReadAction(() => {
DocumentUtils.logicalToLSPPos(editor.getCaretModel.getCurrentCaret.getLogicalPosition, editor)
})
params.setPosition(serverPos)
val future = requestManager.references(params)
if (future != null) {
try {
val references = future.get(REFERENCES_TIMEOUT, TimeUnit.MILLISECONDS)
wrapper.notifySuccess(Timeouts.REFERENCES)
if (references != null) {
invokeLater(() => {
if (!editor.isDisposed) showReferences(references.asScala)
})
}
} catch {
case e: TimeoutException =>
LOG.warn(e)
wrapper.notifyFailure(Timeouts.REFERENCES)
case e@(_: java.io.IOException | _: JsonRpcException | _: ExecutionException) =>
LOG.warn(e)
wrapper.crashed(e.asInstanceOf[Exception])
}
}
}
})
}
private def showReferences(references: Iterable[Location]): Unit = {
var name: String = ""
/**
* Opens the editor and get the required infos
*
* @param file The file of the editor
* @param fileEditorManager The fileEditorManager
* @param start The starting position
* @param end The ending position
* @return (StartOffset, EndOffset, Name (of the symbol), line (containing the symbol))
*/
def openEditorAndGetOffsetsAndName(file: VirtualFile, fileEditorManager: FileEditorManager, start: Position, end: Position): (Int, Int, String, String) = {
val descriptor = new OpenFileDescriptor(project, file)
computableWriteAction(() => {
val newEditor = fileEditorManager.openTextEditor(descriptor, false)
val startOffset = DocumentUtils.LSPPosToOffset(newEditor, start)
val endOffset = DocumentUtils.LSPPosToOffset(newEditor, end)
val doc = newEditor.getDocument
val name = doc.getText(new TextRange(startOffset, endOffset))
fileEditorManager.closeFile(file)
(startOffset, endOffset, name, DocumentUtils.getLineText(newEditor, startOffset, endOffset))
})
}
val locations = references.map(l => {
val start = l.getRange.getStart
val end = l.getRange.getEnd
var startOffset: Int = -1
var endOffset: Int = -1
var sample: String = ""
/**
* Opens the editor to retrieve the offsets, line, etc if needed
*/
def manageUnopenedEditor(): Unit = {
val file = LocalFileSystem.getInstance().findFileByIoFile(new File(new URI(FileUtils.sanitizeURI(l.getUri))))
val fileEditorManager = FileEditorManager.getInstance(project)
if (fileEditorManager.isFileOpen(file)) {
val editors = fileEditorManager.getAllEditors(file).collect { case t: TextEditor => t.getEditor }
if (editors.isEmpty) {
val (s, e, n, sa) = openEditorAndGetOffsetsAndName(file, fileEditorManager, start, end)
startOffset = s
endOffset = e
name = n
sample = sa
} else {
startOffset = DocumentUtils.LSPPosToOffset(editors.head, start)
endOffset = DocumentUtils.LSPPosToOffset(editors.head, end)
}
} else {
val (s, e, n, sa) = openEditorAndGetOffsetsAndName(file, fileEditorManager, start, end)
startOffset = s
endOffset = e
name = n
sample = sa
}
}
EditorEventManager.forUri(l.getUri) match {
case Some(m) =>
try {
startOffset = DocumentUtils.LSPPosToOffset(m.editor, start)
endOffset = DocumentUtils.LSPPosToOffset(m.editor, end)
name = m.editor.getDocument.getText(new TextRange(startOffset, endOffset))
sample = DocumentUtils.getLineText(m.editor, startOffset, endOffset)
} catch {
case e: RuntimeException =>
LOG.warn(e)
manageUnopenedEditor()
}
case None =>
manageUnopenedEditor()
}
(l.getUri, startOffset, endOffset, sample)
}).toArray
val caretPoint = editor.logicalPositionToXY(editor.getCaretModel.getCurrentCaret.getLogicalPosition)
showReferencesWindow(locations, name, caretPoint)
}
/**
* Creates and shows the references window given the locations
*
* @param locations The locations : The file URI, the start offset, end offset, and the sample (line) containing the offsets
* @param name The name of the symbol
* @param point The point at which to show the window
*/
private def showReferencesWindow(locations: Array[(String, Int, Int, String)], name: String, point: Point): Unit = {
if (locations.isEmpty) {
invokeLater(() => if (!editor.isDisposed) currentHint = createAndShowEditorHint(editor, "No usages found", point))
} else {
val frame = new JFrame()
frame.setTitle("Usages of " + name + " (" + locations.length + (if (locations.length > 1) " usages found)" else " usage found"))
val panel = new JPanel()
var row = 0
panel.setLayout(new GridLayoutManager(locations.length, 4, new Insets(10, 10, 10, 10), -1, -1))
locations.foreach(l => {
val listener = new MouseAdapter() {
override def mouseClicked(e: MouseEvent): Unit = {
val file = LocalFileSystem.getInstance().findFileByIoFile(new File(new URI(FileUtils.sanitizeURI(l._1))))
val descriptor = new OpenFileDescriptor(project, file, l._2)
writeAction(() => {
val newEditor = FileEditorManager.getInstance(project).openTextEditor(descriptor, true)
if (l._2 != -1 && l._3 != -1) newEditor.getSelectionModel.setSelection(l._2, l._3)
})
frame.setVisible(false)
frame.dispose()
}
}
val fileLabel = new JLabel(new File(new URI(FileUtils.sanitizeURI(l._1))).getName)
val spacer = new Spacer()
val offsetLabel = new JLabel(l._2.toString)
val sampleLabel = new JLabel("<html>" + l._4 + "</html>")
panel.add(fileLabel, new GridConstraints(row, 0, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false))
panel.add(spacer, new GridConstraints(row, 1, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_CAN_GROW, 1, null, null, null, 0, false))
panel.add(offsetLabel, new GridConstraints(row, 2, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false))
panel.add(sampleLabel, new GridConstraints(row, 3, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false))
row += 1
//TODO refine
fileLabel.addMouseListener(listener)
spacer.addMouseListener(listener)
offsetLabel.addMouseListener(listener)
sampleLabel.addMouseListener(listener)
})
panel.setVisible(true)
frame.setContentPane(panel)
frame.setLocationRelativeTo(editor.getContentComponent)
frame.setLocation(point)
frame.pack()
frame.setAutoRequestFocus(true)
frame.setAlwaysOnTop(true)
frame.setVisible(true)
}
}
/**
* Will show documentation if the mouse doesn't move for a given time (Hover)
*
* @param e the event
*/
def mouseMoved(e: EditorMouseEvent): Unit = {
if (e.getEditor == editor) {
val language = PsiDocumentManager.getInstance(project).getPsiFile(editor.getDocument).getLanguage
if ((EditorSettingsExternalizable.getInstance().isShowQuickDocOnMouseOverElement && //Fixes double doc if documentation provider is present
(LanguageDocumentation.INSTANCE.allForLanguage(language).isEmpty || language.equals(PlainTextLanguage.INSTANCE))) || isCtrlDown) {
val curTime = System.nanoTime()
if (predTime == (-1L) || ctrlTime == (-1L)) {
predTime = curTime
ctrlTime = curTime
} else {
val lPos = getPos(e)
if (lPos != null) {
if (!isKeyPressed || isCtrlDown) {
val offset = editor.logicalPositionToOffset(lPos)
if (isCtrlDown && curTime - ctrlTime > CTRL_THRES) {
if (ctrlRange == null || !ctrlRange.highlightContainsOffset(offset)) {
if (currentHint != null) currentHint.hide()
currentHint = null
if (ctrlRange != null) ctrlRange.dispose()
ctrlRange = null
pool(() => requestAndShowDoc(curTime, lPos, e.getMouseEvent.getPoint))
} else if (ctrlRange.definitionContainsOffset(offset)) {
createAndShowEditorHint(editor, "Click to show usages", editor.offsetToXY(offset))
} else {
editor.getContentComponent.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR))
}
ctrlTime = curTime
} else {
scheduleDocumentation(curTime, lPos, e.getMouseEvent.getPoint)
}
}
}
predTime = curTime
}
}
} else {
LOG.error("Wrong editor for EditorEventManager")
}
}
/**
* Immediately requests the server for documentation at the current editor position
*
* @param editor The editor
*/
def quickDoc(editor: Editor): Unit = {
if (editor == this.editor) {
val caretPos = editor.getCaretModel.getLogicalPosition
val pointPos = editor.logicalPositionToXY(caretPos)
val currentTime = System.nanoTime()
pool(() => requestAndShowDoc(currentTime, caretPos, pointPos))
predTime = currentTime
} else {
LOG.warn("Not same editor!")
}
}
/**
* Gets the hover request and shows it
*
* @param curTime The current time
* @param editorPos The editor position
* @param point The point at which to show the hint
*/
private def requestAndShowDoc(curTime: Long, editorPos: LogicalPosition, point: Point): Unit = {
val serverPos = computableReadAction[Position](() => DocumentUtils.logicalToLSPPos(editorPos, editor))
val request = requestManager.hover(new TextDocumentPositionParams(identifier, serverPos))
if (request != null) {
try {
val hover = request.get(HOVER_TIMEOUT, TimeUnit.MILLISECONDS)
wrapper.notifySuccess(Timeouts.HOVER)
if (hover != null) {
val string = HoverHandler.getHoverString(hover)
if (string != null && string != "") {
if (isCtrlDown) {
invokeLater(() => if (!editor.isDisposed) currentHint = createAndShowEditorHint(editor, string, point, flags = HintManager.HIDE_BY_OTHER_HINT))
createCtrlRange(serverPos, hover.getRange)
} else {
invokeLater(() => if (!editor.isDisposed) currentHint = createAndShowEditorHint(editor, string, point))
}
} else {
LOG.warn("Hover string returned is null for file " + identifier.getUri + " and pos (" + serverPos.getLine + ";" + serverPos.getCharacter + ")")
}
} else {
LOG.warn("Hover is null for file " + identifier.getUri + " and pos (" + serverPos.getLine + ";" + serverPos.getCharacter + ")")
}
} catch {
case e: TimeoutException =>
LOG.warn(e)
wrapper.notifyFailure(Timeouts.HOVER)
case e@(_: java.io.IOException | _: JsonRpcException | _: ExecutionException) =>
LOG.warn(e)
wrapper.crashed(e.asInstanceOf[Exception])
}
}
}
private def createCtrlRange(serverPos: Position, range: Range): Unit = {
val loc = requestDefinition(serverPos)
if (loc != null) {
if (!editor.isDisposed) {
val corRange = if (range == null) {
val params = new TextDocumentPositionParams(identifier, serverPos)
val future = requestManager.documentHighlight(params)
if (future != null) {
try {
val highlights = future.get(DOC_HIGHLIGHT_TIMEOUT, TimeUnit.MILLISECONDS)
if (highlights != null) {
wrapper.notifySuccess(Timeouts.DOC_HIGHLIGHT)
val offset = DocumentUtils.LSPPosToOffset(editor, serverPos)
highlights.asScala.find(dh => DocumentUtils.LSPPosToOffset(editor, dh.getRange.getStart) <= offset
&& offset <= DocumentUtils.LSPPosToOffset(editor, dh.getRange.getEnd)).fold(new Range(serverPos, serverPos))(dh => dh.getRange)
} else new Range(serverPos, serverPos)
} catch {
case e: TimeoutException =>
LOG.warn(e)
wrapper.notifyFailure(Timeouts.DOC_HIGHLIGHT)
new Range(serverPos, serverPos)
case e@(_: java.io.IOException | _: JsonRpcException | _: ExecutionException) =>
LOG.warn(e)
wrapper.crashed(e.asInstanceOf[Exception])
new Range(serverPos, serverPos)
}
} else new Range(serverPos, serverPos)
} else range
val startOffset = DocumentUtils.LSPPosToOffset(editor, corRange.getStart)
val endOffset = DocumentUtils.LSPPosToOffset(editor, corRange.getEnd)
val isDefinition = DocumentUtils.LSPPosToOffset(editor, loc.getRange.getStart) == startOffset
if (ctrlRange != null) ctrlRange.dispose()
ctrlRange = CtrlRangeMarker(loc, editor,
if (!isDefinition) editor.getMarkupModel.addRangeHighlighter(startOffset, endOffset, HighlighterLayer.HYPERLINK, editor.getColorsScheme.getAttributes(EditorColors.REFERENCE_HYPERLINK_COLOR), HighlighterTargetArea.EXACT_RANGE)
else null)
}
}
}
/**
* Returns the position of the definition given a position in the editor
*
* @param position The position
* @return The location of the definition
*/
private def requestDefinition(position: Position): Location = {
val params = new TextDocumentPositionParams(identifier, position)
val request = requestManager.definition(params)
if (request != null) {
try {
val definition = request.get(DEFINITION_TIMEOUT, TimeUnit.MILLISECONDS)
wrapper.notifySuccess(Timeouts.DEFINITION)
if (definition != null && !definition.isEmpty()) {
definition.get(0)
} else {
null
}
} catch {
case e: TimeoutException =>
LOG.warn(e)
wrapper.notifyFailure(Timeouts.DEFINITION)
null
case e@(_: java.io.IOException | _: JsonRpcException | _: ExecutionException) =>
LOG.warn(e)
wrapper.crashed(e.asInstanceOf[Exception])
null
}
} else {
null
}
}
/**
* Returns the references given the position of the word to search for
* Must be called from main thread
*
* @param offset The offset in the editor
* @return An array of PsiElement
*/
def references(offset: Int, getOriginalElement: Boolean = false, close: Boolean = false): (Iterable[PsiElement], Iterable[VirtualFile]) = {
val lspPos = DocumentUtils.offsetToLSPPos(editor, offset)
val params = new ReferenceParams(new ReferenceContext(getOriginalElement))
params.setPosition(lspPos)
params.setTextDocument(identifier)
val request = requestManager.references(params)
if (request != null) {
try {
val res = request.get(REFERENCES_TIMEOUT, TimeUnit.MILLISECONDS)
wrapper.notifySuccess(Timeouts.REFERENCES)
if (res != null) {
val openedEditors = mutable.ListBuffer[VirtualFile]()
val elements = res.asScala.map(l => {
val start = l.getRange.getStart
val end = l.getRange.getEnd
val uri = FileUtils.sanitizeURI(l.getUri)
val file = FileUtils.virtualFileFromURI(uri)
var curEditor = FileUtils.editorFromUri(uri, project)
if (curEditor == null) {
val descriptor = new OpenFileDescriptor(project, file)
curEditor = computableWriteAction(() => FileEditorManager.getInstance(project).openTextEditor(descriptor, false))
openedEditors += file
}
val logicalStart = DocumentUtils.LSPPosToOffset(curEditor, start)
val logicalEnd = DocumentUtils.LSPPosToOffset(curEditor, end)
val name = curEditor.getDocument.getText(new TextRange(logicalStart, logicalEnd))
LSPPsiElement(name, project, logicalStart, logicalEnd, PsiDocumentManager.getInstance(project).getPsiFile(curEditor.getDocument))
.asInstanceOf[PsiElement]
})
if (close) {
writeAction(() => openedEditors.foreach(f => FileEditorManager.getInstance(project).closeFile(f)))
openedEditors.clear()
}
(elements, openedEditors.clone())
} else {
(Seq.empty, Seq.empty)
}
} catch {
case e: TimeoutException =>
LOG.warn(e)
wrapper.notifyFailure(Timeouts.REFERENCES)
(Seq.empty, Seq.empty)
case e@(_: java.io.IOException | _: JsonRpcException | _: ExecutionException) =>
LOG.warn(e)
wrapper.crashed(e.asInstanceOf[Exception])
(Seq.empty, Seq.empty)
}
} else (Seq.empty, Seq.empty)
}
/**
* Reformat the whole document
*/
def reformat(closeAfter: Boolean = false): Unit = {
pool(() => {
if (!editor.isDisposed) {
val params = new DocumentFormattingParams()
params.setTextDocument(identifier)
val options = new FormattingOptions()
params.setOptions(options)
val request = requestManager.formatting(params)
if (request != null) request.thenAccept(formatting => if (formatting != null) invokeLater(() =>
applyEdit(edits = formatting.asScala, name = "Reformat document", closeAfter = closeAfter)))
}
})
}
/**
* Reformat the text currently selected in the editor
*/
def reformatSelection(): Unit = {
pool(() => {
if (!editor.isDisposed) {
val params = new DocumentRangeFormattingParams()
params.setTextDocument(identifier)
val selectionModel = editor.getSelectionModel
val start = computableReadAction(() => selectionModel.getSelectionStart)
val end = computableReadAction(() => selectionModel.getSelectionEnd)
val startingPos = DocumentUtils.offsetToLSPPos(editor, start)
val endPos = DocumentUtils.offsetToLSPPos(editor, end)
params.setRange(new Range(startingPos, endPos))
val options = new FormattingOptions() //TODO
params.setOptions(options)
val request = requestManager.rangeFormatting(params)
if (request != null) request.thenAccept(formatting => if (formatting != null) invokeLater(() => applyEdit(edits = formatting.asScala, name = "Reformat selection")))
}
})
}
/**
* Adds all the listeners
*/
def registerListeners(): Unit = {
editor.addEditorMouseListener(mouseListener)
editor.addEditorMouseMotionListener(mouseMotionListener)
editor.getDocument.addDocumentListener(documentListener)
editor.getSelectionModel.addSelectionListener(selectionListener)
}
/**
* Removes all the listeners
*/
def removeListeners(): Unit = {
editor.removeEditorMouseMotionListener(mouseMotionListener)
editor.getDocument.removeDocumentListener(documentListener)
editor.removeEditorMouseListener(mouseListener)
editor.getSelectionModel.removeSelectionListener(selectionListener)
}
/**
* Rename a symbol in the document
*
* @param renameTo The new name
*/
def rename(renameTo: String, offset: Int = editor.getCaretModel.getCurrentCaret.getOffset): Unit = {
pool(() => {
val servPos = DocumentUtils.offsetToLSPPos(editor, offset)
if (!editor.isDisposed) {
val params = new RenameParams(identifier, servPos, renameTo)
val request = requestManager.rename(params)
if (request != null) request.thenAccept(res => {
WorkspaceEditHandler.applyEdit(res, "Rename to " + renameTo, LSPRenameProcessor.getEditors.toList)
LSPRenameProcessor.clearEditors()
})
}
})
}
/**
* Requests the Hover information
*
* @param editor The editor
* @param offset The offset in the editor
* @return The information
*/
def requestDoc(editor: Editor, offset: Int): String = {
if (editor == this.editor) {
if (offset != -1) {
val serverPos = DocumentUtils.offsetToLSPPos(editor, offset)
val request = requestManager.hover(new TextDocumentPositionParams(identifier, serverPos))
if (request != null) {
try {
val response = request.get(HOVER_TIMEOUT, TimeUnit.MILLISECONDS)
wrapper.notifySuccess(Timeouts.HOVER)
HoverHandler.getHoverString(response)
} catch {
case e: TimeoutException =>
LOG.warn(e)
wrapper.notifyFailure(Timeouts.HOVER)
""
case e@(_: java.io.IOException | _: JsonRpcException | _: ExecutionException) =>
LOG.warn(e)
wrapper.crashed(e.asInstanceOf[Exception])
""
}
} else {
""
}
} else {
LOG.warn("Offset at -1")
""
}
} else {
LOG.warn("Not same editor")
""
}
}
/**
* Manages the change of selected text in the editor
*
* @param e The selection event
*/
def selectionChanged(e: SelectionEvent): Unit = {
if (CodeInsightSettings.getInstance().HIGHLIGHT_IDENTIFIER_UNDER_CARET) {
if (e.getEditor == editor) {
selectedSymbHighlights.foreach(h => editor.getMarkupModel.removeHighlighter(h))
selectedSymbHighlights.clear()
if (editor.getSelectionModel.hasSelection) {
val ideRange = e.getNewRange
val LSPPos = DocumentUtils.offsetToLSPPos(editor, ideRange.getStartOffset)
val request = requestManager.documentHighlight(new TextDocumentPositionParams(identifier, LSPPos))
if (request != null) {
pool(() => {
if (!editor.isDisposed) {
try {
val resp = request.get(DOC_HIGHLIGHT_TIMEOUT, TimeUnit.MILLISECONDS)
wrapper.notifySuccess(Timeouts.DOC_HIGHLIGHT)
if (resp != null) {
invokeLater(() => resp.asScala.foreach(dh => {
if (!editor.isDisposed) {
val range = dh.getRange
val kind = dh.getKind
val startOffset = DocumentUtils.LSPPosToOffset(editor, range.getStart)
val endOffset = DocumentUtils.LSPPosToOffset(editor, range.getEnd)
val colorScheme = editor.getColorsScheme
val highlight = editor.getMarkupModel.addRangeHighlighter(startOffset, endOffset, HighlighterLayer.SELECTION - 1, colorScheme.getAttributes(EditorColors.IDENTIFIER_UNDER_CARET_ATTRIBUTES), HighlighterTargetArea.EXACT_RANGE)
selectedSymbHighlights.add(highlight)
}
}))
}
} catch {
case e: TimeoutException =>
LOG.warn(e)
wrapper.notifyFailure(Timeouts.DOC_HIGHLIGHT)
case e@(_: java.io.IOException | _: JsonRpcException | _: ExecutionException) =>
LOG.warn(e)
wrapper.crashed(e.asInstanceOf[Exception])
}
}
})
}
}
}
}
}
/**
* Tells the manager that the mouse is in the editor
*/
def mouseEntered(): Unit = {
mouseInEditor = true
}
/**
* Tells the manager that the mouse is not in the editor
*/
def mouseExited(): Unit = {
mouseInEditor = false
isCtrlDown = false
}
/**
* Returns the logical position given a mouse event
*
* @param e The event
* @return The position (or null if out of bounds)
*/
private def getPos(e: EditorMouseEvent): LogicalPosition = {
val mousePos = e.getMouseEvent.getPoint
val editorPos = editor.xyToLogicalPosition(mousePos)
val doc = e.getEditor.getDocument
val maxLines = doc.getLineCount
if (editorPos.line >= maxLines) {
null
} else {
val minY = doc.getLineStartOffset(editorPos.line) - (if (editorPos.line > 0) doc.getLineEndOffset(editorPos.line - 1) else 0)
val maxY = doc.getLineEndOffset(editorPos.line) - (if (editorPos.line > 0) doc.getLineEndOffset(editorPos.line - 1) else 0)
if (editorPos.column < minY || editorPos.column > maxY) {
null
} else {
editorPos
}
}
}
/**
* Schedule the documentation using the Timer
*
* @param time The current time
* @param editorPos The position in the editor
* @param point The point where to show the doc
*/
private def scheduleDocumentation(time: Long, editorPos: LogicalPosition, point: Point): Unit = {
if (editorPos != null) {
if (time - predTime > SCHEDULE_THRES) {
try {
hoverThread.schedule(new TimerTask {
override def run(): Unit = {
if (!editor.isDisposed) {
val curTime = System.nanoTime()
if (curTime - predTime > HOVER_TIME_THRES && mouseInEditor && editor.getContentComponent.hasFocus && (!isKeyPressed || isCtrlDown)) {
val editorOffset = computableReadAction[Int](() => editor.logicalPositionToOffset(editorPos))
val inHighlights = diagnosticsHighlights.filter(diag =>
diag.rangeHighlighter.getStartOffset <= editorOffset &&
editorOffset <= diag.rangeHighlighter.getEndOffset)
if (inHighlights.isEmpty || isCtrlDown) {
requestAndShowDoc(curTime, editorPos, point)
}
}
}
}
}, POPUP_THRES)
} catch {
case e: Exception =>
hoverThread = new Timer("Hover", true) //Restart Timer if it crashes
LOG.warn(e)
LOG.warn("Hover timer reset")
}
}
}
}
}