/*
 * Copyright (c) 2019, WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
 *
 * 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 org.wso2.lsp4intellij.editor;

import com.intellij.codeInsight.completion.InsertionContext;
import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer;
import com.intellij.codeInsight.hint.HintManager;
import com.intellij.codeInsight.lookup.AutoCompletionPolicy;
import com.intellij.codeInsight.lookup.LookupElement;
import com.intellij.codeInsight.lookup.LookupElementBuilder;
import com.intellij.codeInsight.template.TemplateManager;
import com.intellij.codeInsight.template.impl.TemplateImpl;
import com.intellij.codeInsight.template.impl.TextExpression;
import com.intellij.lang.Language;
import com.intellij.lang.LanguageDocumentation;
import com.intellij.lang.annotation.Annotation;
import com.intellij.lang.annotation.AnnotationHolder;
import com.intellij.openapi.actionSystem.ActionManager;
import com.intellij.openapi.command.CommandProcessor;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.EditorModificationUtil;
import com.intellij.openapi.editor.LogicalPosition;
import com.intellij.openapi.editor.ScrollType;
import com.intellij.openapi.editor.SelectionModel;
import com.intellij.openapi.editor.colors.EditorColors;
import com.intellij.openapi.editor.event.DocumentEvent;
import com.intellij.openapi.editor.event.DocumentListener;
import com.intellij.openapi.editor.event.EditorMouseEvent;
import com.intellij.openapi.editor.event.EditorMouseListener;
import com.intellij.openapi.editor.event.EditorMouseMotionListener;
import com.intellij.openapi.editor.ex.EditorSettingsExternalizable;
import com.intellij.openapi.editor.markup.HighlighterLayer;
import com.intellij.openapi.editor.markup.HighlighterTargetArea;
import com.intellij.openapi.fileEditor.FileDocumentManager;
import com.intellij.openapi.fileEditor.FileEditorManager;
import com.intellij.openapi.fileEditor.OpenFileDescriptor;
import com.intellij.openapi.fileTypes.PlainTextLanguage;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.VfsUtil;
import com.intellij.openapi.vfs.VfsUtilCore;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiDocumentManager;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.ui.Hint;
import org.apache.commons.lang3.StringUtils;
import org.eclipse.lsp4j.CodeAction;
import org.eclipse.lsp4j.CodeActionContext;
import org.eclipse.lsp4j.CodeActionParams;
import org.eclipse.lsp4j.Command;
import org.eclipse.lsp4j.CompletionItem;
import org.eclipse.lsp4j.CompletionItemKind;
import org.eclipse.lsp4j.CompletionList;
import org.eclipse.lsp4j.CompletionParams;
import org.eclipse.lsp4j.Diagnostic;
import org.eclipse.lsp4j.DidChangeTextDocumentParams;
import org.eclipse.lsp4j.DidCloseTextDocumentParams;
import org.eclipse.lsp4j.DidOpenTextDocumentParams;
import org.eclipse.lsp4j.DidSaveTextDocumentParams;
import org.eclipse.lsp4j.DocumentFormattingParams;
import org.eclipse.lsp4j.DocumentRangeFormattingParams;
import org.eclipse.lsp4j.ExecuteCommandParams;
import org.eclipse.lsp4j.FormattingOptions;
import org.eclipse.lsp4j.Hover;
import org.eclipse.lsp4j.InsertTextFormat;
import org.eclipse.lsp4j.Location;
import org.eclipse.lsp4j.LocationLink;
import org.eclipse.lsp4j.MarkupContent;
import org.eclipse.lsp4j.Position;
import org.eclipse.lsp4j.Range;
import org.eclipse.lsp4j.ReferenceContext;
import org.eclipse.lsp4j.ReferenceParams;
import org.eclipse.lsp4j.RenameParams;
import org.eclipse.lsp4j.SignatureHelp;
import org.eclipse.lsp4j.SignatureInformation;
import org.eclipse.lsp4j.TextDocumentContentChangeEvent;
import org.eclipse.lsp4j.TextDocumentIdentifier;
import org.eclipse.lsp4j.TextDocumentItem;
import org.eclipse.lsp4j.TextDocumentPositionParams;
import org.eclipse.lsp4j.TextDocumentSaveReason;
import org.eclipse.lsp4j.TextDocumentSyncKind;
import org.eclipse.lsp4j.TextEdit;
import org.eclipse.lsp4j.VersionedTextDocumentIdentifier;
import org.eclipse.lsp4j.WillSaveTextDocumentParams;
import org.eclipse.lsp4j.WorkspaceEdit;
import org.eclipse.lsp4j.jsonrpc.JsonRpcException;
import org.eclipse.lsp4j.jsonrpc.messages.Either;
import org.eclipse.lsp4j.jsonrpc.messages.Tuple;
import org.jetbrains.annotations.NotNull;
import org.wso2.lsp4intellij.actions.LSPReferencesAction;
import org.wso2.lsp4intellij.client.languageserver.ServerOptions;
import org.wso2.lsp4intellij.client.languageserver.requestmanager.RequestManager;
import org.wso2.lsp4intellij.client.languageserver.wrapper.LanguageServerWrapper;
import org.wso2.lsp4intellij.contributors.fixes.LSPCodeActionFix;
import org.wso2.lsp4intellij.contributors.fixes.LSPCommandFix;
import org.wso2.lsp4intellij.contributors.icon.LSPIconProvider;
import org.wso2.lsp4intellij.contributors.psi.LSPPsiElement;
import org.wso2.lsp4intellij.contributors.rename.LSPRenameProcessor;
import org.wso2.lsp4intellij.listeners.LSPCaretListenerImpl;
import org.wso2.lsp4intellij.requests.HoverHandler;
import org.wso2.lsp4intellij.requests.Timeouts;
import org.wso2.lsp4intellij.requests.WorkspaceEditHandler;
import org.wso2.lsp4intellij.utils.DocumentUtils;
import org.wso2.lsp4intellij.utils.FileUtils;
import org.wso2.lsp4intellij.utils.GUIUtils;

import java.awt.Cursor;
import java.awt.Point;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.swing.Icon;

import static org.wso2.lsp4intellij.editor.EditorEventManagerBase.getCtrlRange;
import static org.wso2.lsp4intellij.editor.EditorEventManagerBase.getIsCtrlDown;
import static org.wso2.lsp4intellij.editor.EditorEventManagerBase.getIsKeyPressed;
import static org.wso2.lsp4intellij.editor.EditorEventManagerBase.setCtrlRange;
import static org.wso2.lsp4intellij.requests.Timeout.getTimeout;
import static org.wso2.lsp4intellij.requests.Timeouts.CODEACTION;
import static org.wso2.lsp4intellij.requests.Timeouts.COMPLETION;
import static org.wso2.lsp4intellij.requests.Timeouts.DEFINITION;
import static org.wso2.lsp4intellij.requests.Timeouts.EXECUTE_COMMAND;
import static org.wso2.lsp4intellij.requests.Timeouts.HOVER;
import static org.wso2.lsp4intellij.requests.Timeouts.REFERENCES;
import static org.wso2.lsp4intellij.requests.Timeouts.SIGNATURE;
import static org.wso2.lsp4intellij.requests.Timeouts.WILLSAVE;
import static org.wso2.lsp4intellij.utils.ApplicationUtils.computableReadAction;
import static org.wso2.lsp4intellij.utils.ApplicationUtils.computableWriteAction;
import static org.wso2.lsp4intellij.utils.ApplicationUtils.invokeLater;
import static org.wso2.lsp4intellij.utils.ApplicationUtils.pool;
import static org.wso2.lsp4intellij.utils.ApplicationUtils.writeAction;
import static org.wso2.lsp4intellij.utils.GUIUtils.createAndShowEditorHint;

/**
 * Class handling events related to an Editor (a Document)
 * <p>
 * editor              The "watched" editor
 * mouseListener       A listener for mouse clicks
 * mouseMotionListener A listener for mouse movement
 * documentListener    A listener for keystrokes
 * selectionListener   A listener for selection changes in the editor
 * requestManager      The related RequestManager, connected to the right LanguageServer
 * serverOptions       The options of the server regarding completion, signatureHelp, syncKind, etc
 * wrapper             The corresponding LanguageServerWrapper
 */
public class EditorEventManager {

    protected Logger LOG = Logger.getInstance(EditorEventManager.class);

    public Editor editor;
    public LanguageServerWrapper wrapper;
    private Project project;
    private RequestManager requestManager;
    private TextDocumentIdentifier identifier;
    private DocumentListener documentListener;
    private EditorMouseListener mouseListener;
    private EditorMouseMotionListener mouseMotionListener;
    private LSPCaretListenerImpl caretListener;

    public List<String> completionTriggers;
    private List<String> signatureTriggers;
    private DidChangeTextDocumentParams changesParams;
    private TextDocumentSyncKind syncKind;
    private volatile boolean needSave = false;
    private int version = -1;
    private long predTime = -1L;
    private long ctrlTime = -1L;
    private boolean isOpen = false;

    private boolean mouseInEditor = true;
    private Hint currentHint;

    private final List<Diagnostic> diagnostics = new ArrayList<>();
    private AnnotationHolder anonHolder;
    private List<Annotation> annotations = new ArrayList<>();
    private volatile boolean diagnosticSyncRequired = true;
    private volatile boolean codeActionSyncRequired = false;

    public static final String SNIPPET_PLACEHOLDER_REGEX = "\\$\\{\\d+:?([^{^}]*)}";

    //Todo - Revisit arguments order and add remaining listeners
    public EditorEventManager(Editor editor, DocumentListener documentListener, EditorMouseListener mouseListener,
                              EditorMouseMotionListener mouseMotionListener, LSPCaretListenerImpl caretListener,
                              RequestManager requestManager, ServerOptions serverOptions, LanguageServerWrapper wrapper) {

        this.editor = editor;
        this.documentListener = documentListener;
        this.mouseListener = mouseListener;
        this.mouseMotionListener = mouseMotionListener;
        this.requestManager = requestManager;
        this.wrapper = wrapper;
        this.caretListener = caretListener;

        this.identifier = new TextDocumentIdentifier(FileUtils.editorToURIString(editor));
        this.changesParams = new DidChangeTextDocumentParams(new VersionedTextDocumentIdentifier(),
                Collections.singletonList(new TextDocumentContentChangeEvent()));
        this.syncKind = serverOptions.syncKind;

        this.completionTriggers = (serverOptions.completionOptions != null
                && serverOptions.completionOptions.getTriggerCharacters() != null) ?
                serverOptions.completionOptions.getTriggerCharacters() :
                new ArrayList<>();

        this.signatureTriggers = (serverOptions.signatureHelpOptions != null
                && serverOptions.signatureHelpOptions.getTriggerCharacters() != null) ?
                serverOptions.signatureHelpOptions.getTriggerCharacters() :
                new ArrayList<>();

        this.project = editor.getProject();

        EditorEventManagerBase.uriToManager.put(FileUtils.editorToURIString(editor), this);
        EditorEventManagerBase.editorToManager.put(editor, this);
        changesParams.getTextDocument().setUri(identifier.getUri());

        this.currentHint = null;
    }

    @SuppressWarnings("unused")
    public Project getProject() {
        return project;
    }

    @SuppressWarnings("unused")
    public RequestManager getRequestManager() {
        return requestManager;
    }

    @SuppressWarnings("unused")
    public TextDocumentIdentifier getIdentifier() {
        return identifier;
    }

    @SuppressWarnings("unused")
    public DidChangeTextDocumentParams getChangesParams() {
        return changesParams;
    }

    /**
     * Calls onTypeFormatting or signatureHelp if the character typed was a trigger character
     *
     * @param c The character just typed
     */
    public void characterTyped(char c) {
        if (signatureTriggers.contains(Character.toString(c))) {
            signatureHelp();
        }
    }

    /**
     * Tells the manager that the mouse is in the editor
     */
    public void mouseEntered() {
        mouseInEditor = true;
    }

    /**
     * Tells the manager that the mouse is not in the editor
     */
    public void mouseExited() {
        mouseInEditor = false;
    }

    /**
     * Will show documentation if the mouse doesn't move for a given time (Hover)
     *
     * @param e the event
     */
    public void mouseMoved(EditorMouseEvent e) {

        if (e.getEditor() != editor) {
            LOG.error("Wrong editor for EditorEventManager");
            return;
        }

        PsiFile psiFile = PsiDocumentManager.getInstance(project).getPsiFile(editor.getDocument());
        if (psiFile == null) {
            return;
        }
        Language language = psiFile.getLanguage();
        if ((!LanguageDocumentation.INSTANCE.allForLanguage(language).isEmpty() && !isSupportedLanguageFile(psiFile))
                || (!getIsCtrlDown() && !EditorSettingsExternalizable.getInstance().isShowQuickDocOnMouseOverElement())) {
            return;
        }

        long curTime = System.nanoTime();
        if (predTime == (-1L) || ctrlTime == (-1L)) {
            predTime = curTime;
            ctrlTime = curTime;
        } else {
            LogicalPosition lPos = getPos(e);
            if (lPos == null || getIsKeyPressed() && !getIsCtrlDown()) {
                return;
            }

            int offset = editor.logicalPositionToOffset(lPos);
            if (getIsCtrlDown() && curTime - ctrlTime > EditorEventManagerBase.CTRL_THRESH) {
                if (getCtrlRange() == null || !getCtrlRange().highlightContainsOffset(offset)) {
                    if (currentHint != null) {
                        currentHint.hide();
                    }
                    currentHint = null;
                    if (getCtrlRange() != null) {
                        getCtrlRange().dispose();
                    }
                    setCtrlRange(null);
                    pool(() -> requestAndShowDoc(lPos, e.getMouseEvent().getPoint()));
                } else if (getCtrlRange().definitionContainsOffset(offset)) {
                    createAndShowEditorHint(editor, "Click to show usages", editor.offsetToXY(offset));
                } else {
                    editor.getContentComponent().setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
                }
                ctrlTime = curTime;
            }
            predTime = curTime;
        }
    }

    private boolean isSupportedLanguageFile(PsiFile file) {
        return file.getLanguage().isKindOf(PlainTextLanguage.INSTANCE)
                || FileUtils.isFileSupported(file.getVirtualFile());
    }

    /**
     * Called when the mouse is clicked
     * At the moment, is used by CTRL+click to see references / goto definition
     *
     * @param e The mouse event
     */
    public void mouseClicked(EditorMouseEvent e) {
        if (e.getEditor() != editor) {
            LOG.error("Wrong editor for EditorEventManager");
            return;
        }

        if (getIsCtrlDown()) {
            // If CTRL/CMD key is pressed, triggers goto definition/references and hover.
            try {
                trySourceNavigationAndHover(e);
            } catch (Exception err) {
                LOG.warn("Error occurred when trying source navigation", err);
            }
        }
    }

    private void createCtrlRange(Position logicalPos, Range range) {
        Location location = requestDefinition(logicalPos);
        if (location == null || location.getRange() == null || editor.isDisposed()) {
            return;
        }
        Range corRange;
        if (range == null) {
            corRange = new Range(logicalPos, logicalPos);
        } else {
            corRange = range;
        }
        int startOffset = DocumentUtils.LSPPosToOffset(editor, corRange.getStart());
        int endOffset = DocumentUtils.LSPPosToOffset(editor, corRange.getEnd());
        boolean isDefinition = DocumentUtils.LSPPosToOffset(editor, location.getRange().getStart()) == startOffset;

        CtrlRangeMarker ctrlRange = getCtrlRange();
        if (!editor.isDisposed()) {
            if (ctrlRange != null) {
                ctrlRange.dispose();
            }
            setCtrlRange(new CtrlRangeMarker(location, editor, !isDefinition ?
                    (editor.getMarkupModel().addRangeHighlighter(startOffset, endOffset, HighlighterLayer.HYPERLINK,
                            editor.getColorsScheme().getAttributes(EditorColors.REFERENCE_HYPERLINK_COLOR),
                            HighlighterTargetArea.EXACT_RANGE)) : null));
        }
    }

    /**
     * Returns the position of the definition given a position in the editor
     *
     * @param position The position
     * @return The location of the definition
     */
    private Location requestDefinition(Position position) {
        TextDocumentPositionParams params = new TextDocumentPositionParams(identifier, position);
        CompletableFuture<Either<List<? extends Location>, List<? extends LocationLink>>> request =
                requestManager.definition(params);

        if (request == null) {
            return null;
        }
        try {
            // for now we only get Location, so we only check the left, but in future we might need to support
            // right as well which will return LocationLink
            Either<List<? extends Location>, List<? extends LocationLink>> definition =
                    request.get(getTimeout(DEFINITION), TimeUnit.MILLISECONDS);
            wrapper.notifySuccess(Timeouts.DEFINITION);
            if (definition.isLeft() && !definition.getLeft().isEmpty()) {
                return definition.getLeft().get(0);
            }
        } catch (TimeoutException e) {
            LOG.warn(e);
            wrapper.notifyFailure(Timeouts.DEFINITION);
            return null;
        } catch (InterruptedException | JsonRpcException | ExecutionException e) {
            LOG.warn(e);
            wrapper.crashed(e);
            return null;
        }
        return null;
    }

    public Pair<List<PsiElement>, List<VirtualFile>> references(int offset) {
        return references(offset, false, false);
    }

    /**
     * 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
     */
    public Pair<List<PsiElement>, List<VirtualFile>> references(int offset, boolean getOriginalElement, boolean close) {
        Position lspPos = DocumentUtils.offsetToLSPPos(editor, offset);
        ReferenceParams params = new ReferenceParams(new ReferenceContext(getOriginalElement));
        params.setPosition(lspPos);
        params.setTextDocument(identifier);
        CompletableFuture<List<? extends Location>> request = requestManager.references(params);
        if (request != null) {
            try {
                List<? extends Location> res = request.get(getTimeout(REFERENCES), TimeUnit.MILLISECONDS);
                wrapper.notifySuccess(Timeouts.REFERENCES);
                if (res != null && res.size() > 0) {
                    List<VirtualFile> openedEditors = new ArrayList<>();
                    List<PsiElement> elements = new ArrayList<>();
                    res.forEach(l -> {
                        Position start = l.getRange().getStart();
                        Position end = l.getRange().getEnd();
                        String uri = FileUtils.sanitizeURI(l.getUri());
                        VirtualFile file = FileUtils.virtualFileFromURI(uri);
                        Editor curEditor = FileUtils.editorFromUri(uri, project);
                        if (curEditor == null && file != null) {
                            OpenFileDescriptor descriptor = new OpenFileDescriptor(project, file, start.getLine(), start.getCharacter());
                            curEditor = computableWriteAction(
                                    () -> FileEditorManager.getInstance(project).openTextEditor(descriptor, false));
                            openedEditors.add(file);
                        }
                        if (curEditor == null) {
                            LOG.warn("Error occurred in LSP references.");
                            return;
                        }
                        int logicalStart = DocumentUtils.LSPPosToOffset(curEditor, start);
                        int logicalEnd = DocumentUtils.LSPPosToOffset(curEditor, end);
                        String name = curEditor.getDocument().getText(new TextRange(logicalStart, logicalEnd));
                        elements.add(new LSPPsiElement(name, project, logicalStart, logicalEnd,
                                PsiDocumentManager.getInstance(project).getPsiFile(curEditor.getDocument())));
                    });
                    if (close) {
                        writeAction(
                                () -> openedEditors.forEach(f -> FileEditorManager.getInstance(project).closeFile(f)));
                        openedEditors.clear();
                    }
                    return new Pair<>(elements, openedEditors);
                } else {
                    return new Pair<>(null, null);
                }
            } catch (TimeoutException e) {
                LOG.warn(e);
                wrapper.notifyFailure(Timeouts.REFERENCES);
                return new Pair<>(null, null);
            } catch (InterruptedException | JsonRpcException | ExecutionException e) {
                LOG.warn(e);
                wrapper.crashed(e);
                return new Pair<>(null, null);
            }
        }
        return new Pair<>(null, null);
    }

    /**
     * @return The current diagnostics highlights
     */
    public synchronized List<Diagnostic> getDiagnostics() {
        this.diagnosticSyncRequired = false;
        return this.diagnostics;
    }

    /**
     * @return The current diagnostic annotations
     */
    public synchronized List<Annotation> getAnnotations() {
        this.codeActionSyncRequired = false;
        return this.annotations;
    }

    public synchronized void setAnnotations(List<Annotation> annotations) {
        this.annotations = annotations;
    }

    public synchronized void setAnonHolder(AnnotationHolder holder) {
        this.anonHolder = holder;
    }

    public synchronized boolean isDiagnosticSyncRequired() {
        return this.diagnosticSyncRequired;
    }

    public synchronized boolean isCodeActionSyncRequired() {
        return this.codeActionSyncRequired;
    }

    /**
     * Applies the diagnostics to the document
     *
     * @param diagnostics The diagnostics to apply from the server
     */
    public void diagnostics(List<Diagnostic> diagnostics) {

        // If both of the old diagnostics and the received diagnostics are empty, we can simply return without
        // re-triggering the annotator.
        if (editor.isDisposed() || (this.diagnostics.isEmpty() && diagnostics.isEmpty())) {
            return;
        }

        synchronized (this.diagnostics) {
            this.diagnostics.clear();
            this.diagnostics.addAll(diagnostics);
            diagnosticSyncRequired = true;
            // Triggers force full DaemonCodeAnalyzer execution.
            updateErrorAnnotations();
        }
    }

    /**
     * Retrieves the commands needed to apply a CodeAction
     *
     * @param offset The cursor position(offset) which should be evaluated for code action request.
     * @return The list of commands, or null if none are given / the request times out
     */
    @SuppressWarnings("WeakerAccess")
    public List<Either<Command, CodeAction>> codeAction(int offset) {
        CodeActionParams params = new CodeActionParams();
        params.setTextDocument(identifier);
        Range range = new Range(DocumentUtils.offsetToLSPPos(editor, offset),
                DocumentUtils.offsetToLSPPos(editor, offset));
        params.setRange(range);

        // Calculates the diagnostic context.
        List<Diagnostic> diagnosticContext = new ArrayList<>();
        diagnostics.forEach(diagnostic -> {
            int startOffset = DocumentUtils.LSPPosToOffset(editor, diagnostic.getRange().getStart());
            int endOffset = DocumentUtils.LSPPosToOffset(editor, diagnostic.getRange().getEnd());
            if (offset >= startOffset && offset <= endOffset) {
                diagnosticContext.add(diagnostic);
            }
        });

        CodeActionContext context = new CodeActionContext(diagnosticContext);
        params.setContext(context);
        CompletableFuture<List<Either<Command, CodeAction>>> future = requestManager.codeAction(params);
        if (future != null) {
            try {
                List<Either<Command, CodeAction>> res = future.get(getTimeout(CODEACTION), TimeUnit.MILLISECONDS);
                wrapper.notifySuccess(CODEACTION);
                return res;
            } catch (TimeoutException e) {
                LOG.warn(e);
                wrapper.notifyFailure(CODEACTION);
                return null;
            } catch (InterruptedException | JsonRpcException | ExecutionException e) {
                LOG.warn(e);
                wrapper.crashed(e);
                return null;
            }
        }
        return null;
    }

    /**
     * Calls signatureHelp at the current editor caret position
     */
    @SuppressWarnings("WeakerAccess")
    public void signatureHelp() {
        if (editor.isDisposed()) {
            return;
        }
        LogicalPosition lPos = editor.getCaretModel().getCurrentCaret().getLogicalPosition();
        Point point = editor.logicalPositionToXY(lPos);
        TextDocumentPositionParams params = new TextDocumentPositionParams(identifier, DocumentUtils.logicalToLSPPos(lPos, editor));
        pool(() -> {
            CompletableFuture<SignatureHelp> future = requestManager.signatureHelp(params);
            if (future == null) {
                return;
            }
            try {
                SignatureHelp signatureResp = future.get(getTimeout(SIGNATURE), TimeUnit.MILLISECONDS);
                wrapper.notifySuccess(Timeouts.SIGNATURE);
                if (signatureResp == null) {
                    return;
                }
                List<SignatureInformation> signatures = signatureResp.getSignatures();
                if (signatures == null || signatures.isEmpty()) {
                    return;
                }
                int activeSignatureIndex = signatureResp.getActiveSignature();
                int activeParameterIndex = signatureResp.getActiveParameter();

                String activeParameter = signatures.get(activeSignatureIndex).getParameters().size() > activeParameterIndex ?
                        extractLabel(signatures.get(activeSignatureIndex), signatures.get(activeSignatureIndex).getParameters().get(activeParameterIndex).getLabel()) : "";
                Either<String, MarkupContent> signatureDescription = signatures.get(activeSignatureIndex).getDocumentation();

                StringBuilder builder = new StringBuilder();
                builder.append("<html>");
                if (signatureDescription == null) {
                    builder.append("<b>").append(signatures.get(activeSignatureIndex).getLabel().
                            replace(" " + activeParameter, String.format("<font color=\"orange\"> %s</font>",
                                    activeParameter))).append("</b>");
                } else if (signatureDescription.isLeft()) {
                    // Todo - Add parameter Documentation
                    String descriptionLeft = signatureDescription.getLeft().replace(System.lineSeparator(), "<br />");
                    builder.append("<b>").append(signatures.get(activeSignatureIndex).getLabel()
                            .replace(" " + activeParameter, String.format("<font color=\"orange\"> %s</font>",
                                    activeParameter))).append("</b>");
                    builder.append("<div>").append(descriptionLeft).append("</div>");
                } else if (signatureDescription.isRight()) {
                    // Todo - Add marked content parsing
                    builder.append("<b>").append(signatures.get(activeSignatureIndex).getLabel()).append("</b>");
                }

                builder.append("</html>");
                invokeLater(() -> currentHint = createAndShowEditorHint(editor, builder.toString(), point, HintManager.UNDER, HintManager.HIDE_BY_OTHER_HINT));

            } catch (TimeoutException e) {
                LOG.warn(e);
                wrapper.notifyFailure(Timeouts.SIGNATURE);
            } catch (JsonRpcException | ExecutionException | InterruptedException e) {
                LOG.warn(e);
                wrapper.crashed(e);
            } catch (Exception e) {
                LOG.warn("Internal error occurred when processing signature help");
            }
        });
    }

    private String extractLabel(SignatureInformation signatureInformation, Either<String, Tuple.Two<Integer, Integer>> label) {
        if (label.isLeft()) {
            return label.getLeft();
        } else if (label.isRight()) {
            return signatureInformation.getLabel().substring(label.getRight().getFirst(), label.getRight().getSecond());
        } else {
            return "";
        }
    }

    /**
     * Reformat the whole document
     */
    public void reformat() {
        pool(() -> {
            if (editor.isDisposed()) {
                return;
            }
            DocumentFormattingParams params = new DocumentFormattingParams();
            params.setTextDocument(identifier);
            FormattingOptions options = new FormattingOptions();
            params.setOptions(options);

            CompletableFuture<List<? extends TextEdit>> request = requestManager.formatting(params);
            if (request == null) {
                return;
            }
            request.thenAccept(formatting -> {
                if (formatting != null) {
                    invokeLater(() -> applyEdit((List<TextEdit>) formatting, "Reformat document", false));
                }
            });
        });
    }

    /**
     * Reformat the text currently selected in the editor
     */
    public void reformatSelection() {
        pool(() -> {
            if (editor.isDisposed()) {
                return;
            }
            DocumentRangeFormattingParams params = new DocumentRangeFormattingParams();
            params.setTextDocument(identifier);
            SelectionModel selectionModel = editor.getSelectionModel();
            int start = computableReadAction(selectionModel::getSelectionStart);
            int end = computableReadAction(selectionModel::getSelectionEnd);
            Position startingPos = DocumentUtils.offsetToLSPPos(editor, start);
            Position endPos = DocumentUtils.offsetToLSPPos(editor, end);
            params.setRange(new Range(startingPos, endPos));
            // Todo - Make Formatting Options configurable
            FormattingOptions options = new FormattingOptions();
            params.setOptions(options);

            CompletableFuture<List<? extends TextEdit>> request = requestManager.rangeFormatting(params);
            if (request == null) {
                return;
            }
            request.thenAccept(formatting -> {
                if (formatting == null) {
                    return;
                }
                invokeLater(() -> {
                    if (!editor.isDisposed()) {
                        applyEdit((List<TextEdit>) formatting, "Reformat selection", false);
                    }
                });
            });
        });
    }

    public void rename(String renameTo) {
        rename(renameTo, editor.getCaretModel().getCurrentCaret().getOffset());
    }

    /**
     * Rename a symbol in the document
     *
     * @param renameTo The new name
     */
    public void rename(String renameTo, int offset) {
        pool(() -> {
            if (editor.isDisposed()) {
                return;
            }
            Position servPos = DocumentUtils.offsetToLSPPos(editor, offset);
            RenameParams params = new RenameParams(identifier, servPos, renameTo);
            CompletableFuture<WorkspaceEdit> request = requestManager.rename(params);
            if (request != null) {
                request.thenAccept(res -> {
                    WorkspaceEditHandler
                            .applyEdit(res, "Rename to " + renameTo, new ArrayList<>(LSPRenameProcessor.getEditors()));
                    LSPRenameProcessor.clearEditors();
                });
            }
        });
    }

    /**
     * Immediately requests the server for documentation at the current editor position
     *
     * @param editor The editor
     */
    public void quickDoc(Editor editor) {
        if (editor == this.editor) {
            LogicalPosition caretPos = editor.getCaretModel().getLogicalPosition();
            Point pointPos = editor.logicalPositionToXY(caretPos);
            long currentTime = System.nanoTime();
            pool(() -> requestAndShowDoc(caretPos, pointPos));
            predTime = currentTime;
        } else {
            LOG.warn("Not same editor!");
        }
    }

    /**
     * Gets the hover request and shows it
     *
     * @param editorPos The editor position
     * @param point     The point at which to show the hint
     */
    private void requestAndShowDoc(LogicalPosition editorPos, Point point) {
        Position serverPos = computableReadAction(() -> DocumentUtils.logicalToLSPPos(editorPos, editor));
        CompletableFuture<Hover> request = requestManager.hover(new TextDocumentPositionParams(identifier, serverPos));
        if (request == null) {
            return;
        }
        try {
            Hover hover = request.get(getTimeout(HOVER), TimeUnit.MILLISECONDS);
            wrapper.notifySuccess(Timeouts.HOVER);

            if (hover == null) {
                LOG.warn(String.format("Hover is null for file %s and pos (%d;%d)", identifier.getUri(),
                        serverPos.getLine(), serverPos.getCharacter()));
                return;
            }

            String string = HoverHandler.getHoverString(hover);
            if (StringUtils.isEmpty(string)) {
                LOG.warn(String.format("Hover string returned is null for file %s and pos (%d;%d)",
                        identifier.getUri(), serverPos.getLine(), serverPos.getCharacter()));
                return;
            }

            if (getIsCtrlDown()) {
                invokeLater(() -> {
                    if (!editor.isDisposed()) {
                        currentHint = createAndShowEditorHint(editor, string, point, HintManager.HIDE_BY_OTHER_HINT);
                    }
                });
            } else {
                invokeLater(() -> {
                    if (!editor.isDisposed()) {
                        currentHint = createAndShowEditorHint(editor, string, point);
                    }
                });
            }
        } catch (TimeoutException e) {
            LOG.warn(e);
            wrapper.notifyFailure(Timeouts.HOVER);
        } catch (InterruptedException | JsonRpcException | ExecutionException e) {
            LOG.warn(e);
            wrapper.crashed(e);
        }
    }

    /**
     * Returns the completion suggestions given a position
     *
     * @param pos The LSP position
     * @return The suggestions
     */
    public Iterable<? extends LookupElement> completion(Position pos) {

        List<LookupElement> lookupItems = new ArrayList<>();
        CompletableFuture<Either<List<CompletionItem>, CompletionList>> request = requestManager
                .completion(new CompletionParams(identifier, pos));
        if (request == null) {
            return lookupItems;
        }

        try {
            Either<List<CompletionItem>, CompletionList> res = request.get(getTimeout(COMPLETION), TimeUnit.MILLISECONDS);
            wrapper.notifySuccess(Timeouts.COMPLETION);
            if (res == null) {
                return lookupItems;
            }
            if (res.getLeft() != null) {
                for (CompletionItem item : res.getLeft()) {
                    LookupElement lookupElement = createLookupItem(item);
                    if (lookupElement != null) {
                        lookupItems.add(lookupElement);
                    }
                }
            } else if (res.getRight() != null) {
                for (CompletionItem item : res.getRight().getItems()) {
                    LookupElement lookupElement = createLookupItem(item);
                    if (lookupElement != null) {
                        lookupItems.add(lookupElement);
                    }
                }
            }
        } catch (TimeoutException | InterruptedException e) {
            LOG.warn(e);
            wrapper.notifyFailure(Timeouts.COMPLETION);
        } catch (JsonRpcException | ExecutionException e) {
            LOG.warn(e);
            wrapper.crashed(e);
        } finally {
            return lookupItems;
        }
    }

    /**
     * Creates a LookupElement given a CompletionItem
     *
     * @param item The CompletionItem
     * @return The corresponding LookupElement
     */
    @SuppressWarnings("WeakerAccess")
    public LookupElement createLookupItem(CompletionItem item) {
        Command command = item.getCommand();
        String detail = item.getDetail();
        String insertText = item.getInsertText();
        CompletionItemKind kind = item.getKind();
        String label = item.getLabel();
        TextEdit textEdit = item.getTextEdit();
        List<TextEdit> addTextEdits = item.getAdditionalTextEdits();
        String presentableText = StringUtils.isNotEmpty(label) ? label : (insertText != null) ? insertText : "";
        String tailText = (detail != null) ? detail : "";
        LSPIconProvider iconProvider = GUIUtils.getIconProviderFor(wrapper.getServerDefinition());
        Icon icon = iconProvider.getCompletionIcon(kind);
        LookupElementBuilder lookupElementBuilder;

        String lookupString = null;
        if (textEdit != null) {
            lookupString = textEdit.getNewText();
        } else if (StringUtils.isNotEmpty(insertText)) {
            lookupString = insertText;
        } else if (StringUtils.isNotEmpty(label)) {
            lookupString = label;
        }
        if (StringUtils.isEmpty(lookupString)) {
            return null;
        }
        // Fixes IDEA internal assertion failure in windows.
        lookupString = lookupString.replace(DocumentUtils.WIN_SEPARATOR, DocumentUtils.LINUX_SEPARATOR);
        if (item.getInsertTextFormat() == InsertTextFormat.Snippet) {
            lookupElementBuilder = LookupElementBuilder.create(convertPlaceHolders(lookupString));
        } else {
            lookupElementBuilder = LookupElementBuilder.create(lookupString);
        }

        lookupElementBuilder = addCompletionInsertHandlers(item, lookupElementBuilder, lookupString);

        if (kind == CompletionItemKind.Keyword) {
            lookupElementBuilder = lookupElementBuilder.withBoldness(true);
        }

        return lookupElementBuilder.withPresentableText(presentableText).withTypeText(tailText, true).withIcon(icon)
                .withAutoCompletionPolicy(AutoCompletionPolicy.SETTINGS_DEPENDENT);
    }

    @SuppressWarnings("WeakerAccess")
    public LookupElementBuilder addCompletionInsertHandlers(CompletionItem item, LookupElementBuilder builder, String lookupString) {

        String label = item.getLabel();
        Command command = item.getCommand();
        List<TextEdit> addTextEdits = item.getAdditionalTextEdits();
        InsertTextFormat format = item.getInsertTextFormat();

        if (addTextEdits != null) {
            builder = builder.withInsertHandler((InsertionContext context, LookupElement lookupElement) -> invokeLater(() -> {
                if (format == InsertTextFormat.Snippet) {
                    context.commitDocument();
                    prepareAndRunSnippet(lookupString);
                }

                context.commitDocument();
                applyEdit(Integer.MAX_VALUE, addTextEdits, "Completion : " + label, false, false);
                if (command != null) {
                    executeCommands(Collections.singletonList(command));
                }
            }));
        } else if (command != null) {
            builder = builder.withInsertHandler((InsertionContext context, LookupElement lookupElement) -> {
                if (format == InsertTextFormat.Snippet) {
                    context.commitDocument();
                    prepareAndRunSnippet(lookupString);
                }
                context.commitDocument();
                executeCommands(Collections.singletonList(command));
            });
        } else {
            builder = builder.withInsertHandler((InsertionContext context, LookupElement lookupElement) -> {
                if (format == InsertTextFormat.Snippet) {
                    context.commitDocument();
                    prepareAndRunSnippet(lookupString);
                }
            });
        }

        return builder;
    }

    @SuppressWarnings("WeakerAccess")
    public void prepareAndRunSnippet(String insertText) {

        List<SnippetVariable> variables = new ArrayList<>();
        // Extracts variables using placeholder REGEX pattern.
        Matcher varMatcher = Pattern.compile(SNIPPET_PLACEHOLDER_REGEX).matcher(insertText);
        while (varMatcher.find()) {
            variables.add(new SnippetVariable(varMatcher.group(), varMatcher.start(), varMatcher.end()));
        }

        variables.sort(Comparator.comparingInt(o -> o.startIndex));
        final String[] finalInsertText = {insertText};
        variables.forEach(var -> finalInsertText[0] = finalInsertText[0].replace(var.lspSnippetText, "$"));

        String[] splitInsertText = finalInsertText[0].split("\\$");
        finalInsertText[0] = String.join("", splitInsertText);

        TemplateImpl template = (TemplateImpl) TemplateManager.getInstance(getProject()).createTemplate(finalInsertText[0],
                "lsp4intellij");
        template.parseSegments();

        final int[] varIndex = {0};
        variables.forEach(var -> {
            template.addTextSegment(splitInsertText[varIndex[0]]);
            template.addVariable(varIndex[0] + "_" + var.variableValue, new TextExpression(var.variableValue),
                    new TextExpression(var.variableValue), true, false);
            varIndex[0]++;
        });
        // If the snippet text ends with a placeholder, there will be no string segment left to append after the last
        // variable.
        if (splitInsertText.length != variables.size()) {
            template.addTextSegment(splitInsertText[splitInsertText.length - 1]);
        }
        template.setInline(true);
        EditorModificationUtil.moveCaretRelatively(editor, -template.getTemplateText().length());
        TemplateManager.getInstance(getProject()).startTemplate(editor, template);
    }

    private String convertPlaceHolders(String insertText) {
        return insertText.replaceAll(SNIPPET_PLACEHOLDER_REGEX, "");
    }

    /**
     * Returns the logical position given a mouse event
     *
     * @param e The event
     * @return The position (or null if out of bounds)
     */
    private LogicalPosition getPos(EditorMouseEvent e) {
        Point mousePos = e.getMouseEvent().getPoint();
        LogicalPosition editorPos = editor.xyToLogicalPosition(mousePos);
        Document doc = e.getEditor().getDocument();
        int maxLines = doc.getLineCount();
        if (editorPos.line >= maxLines) {
            return null;
        } else {
            int minY = doc.getLineStartOffset(editorPos.line) - (editorPos.line > 0 ?
                    doc.getLineEndOffset(editorPos.line - 1) : 0);
            int maxY = doc.getLineEndOffset(editorPos.line) - (editorPos.line > 0 ?
                    doc.getLineEndOffset(editorPos.line - 1) : 0);
            return (editorPos.column > minY && editorPos.column < maxY) ? editorPos : null;
        }
    }

    boolean applyEdit(List<TextEdit> edits, String name, boolean setCaret) {
        return applyEdit(Integer.MAX_VALUE, edits, name, false, setCaret);
    }

    /**
     * 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
     */
    boolean applyEdit(int version, List<TextEdit> edits, String name, boolean closeAfter, boolean setCaret) {
        Runnable runnable = getEditsRunnable(version, edits, name, setCaret);
        writeAction(() -> {
            if (runnable != null) {
                CommandProcessor.getInstance()
                        .executeCommand(project, runnable, name, "LSPPlugin", editor.getDocument());
            }
            if (closeAfter) {
                PsiFile file = PsiDocumentManager.getInstance(project).getPsiFile(editor.getDocument());
                if (file != null) {
                    FileEditorManager.getInstance(project).closeFile(file.getVirtualFile());
                }
            }
        });
        return runnable != null;
    }

    /**
     * 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
     */
    public Runnable getEditsRunnable(int version, List<TextEdit> edits, String name, boolean setCaret) {
        if (version < this.version) {
            LOG.warn(String.format("Edit version %d is older than current version %d", version, this.version));
            return null;
        }
        if (edits == null) {
            LOG.warn("Received edits list is null.");
            return null;
        }
        Document document = editor.getDocument();
        if (!document.isWritable()) {
            LOG.warn("Document is not writable");
            return null;
        }

        return () -> {
            // Creates a sorted edit list based on the insertion position and the edits will be applied from the bottom
            // to the top of the document. Otherwise all the other edit ranges will be invalid after the very first edit,
            // since the document is changed.
            List<LSPTextEdit> lspEdits = new ArrayList<>();
            edits.forEach(edit -> {
                String text = edit.getNewText();
                Range range = edit.getRange();

                if (range != null && StringUtils.isNotEmpty(text)) {
                    int start = DocumentUtils.LSPPosToOffset(editor, range.getStart());
                    int end = DocumentUtils.LSPPosToOffset(editor, range.getEnd());
                    lspEdits.add(new LSPTextEdit(text, start, end));
                }
            });

            // Sort according to the start offset, in descending order.
            Collections.sort(lspEdits);

            lspEdits.forEach(edit -> {
                String text = edit.getText();
                int start = edit.getStartOffset();
                int end = edit.getEndOffset();
                if (StringUtils.isEmpty(text)) {
                    document.deleteString(start, end);
                } else {
                    text = text.replace(DocumentUtils.WIN_SEPARATOR, DocumentUtils.LINUX_SEPARATOR);
                    if (end >= 0) {
                        if (end - start <= 0) {
                            document.insertString(start, text);
                        } else {
                            document.replaceString(start, end, text);
                        }
                    } else if (start == 0) {
                        document.setText(text);
                    } else if (start > 0) {
                        document.insertString(start, text);
                    }
                    if (setCaret) {
                        editor.getCaretModel().moveToOffset(start + text.length());
                    }
                }
                saveDocument();
            });
        };
    }

    /**
     * Sends commands to execute to the server and applies the changes returned if the future returns a WorkspaceEdit
     *
     * @param commands The commands to execute
     */
    public void executeCommands(List<Command> commands) {
        pool(() -> {
            if (editor.isDisposed()) {
                return;
            }
            commands.stream().map(c -> {
                ExecuteCommandParams params = new ExecuteCommandParams();
                params.setArguments(c.getArguments());
                params.setCommand(c.getCommand());
                return requestManager.executeCommand(params);
            }).filter(Objects::nonNull).forEach(f -> {
                try {
                    f.get(getTimeout(EXECUTE_COMMAND), TimeUnit.MILLISECONDS);
                    wrapper.notifySuccess(Timeouts.EXECUTE_COMMAND);
                } catch (TimeoutException te) {
                    LOG.warn(te);
                    wrapper.notifyFailure(Timeouts.EXECUTE_COMMAND);
                } catch (JsonRpcException | ExecutionException | InterruptedException e) {
                    LOG.warn(e);
                    wrapper.crashed(e);
                }
            });
        });
    }

    private void saveDocument() {
        FileDocumentManager.getInstance().saveDocument(editor.getDocument());
    }

    /**
     * Adds all the listeners
     */
    public void registerListeners() {
        editor.getDocument().addDocumentListener(documentListener);
        editor.addEditorMouseListener(mouseListener);
        editor.addEditorMouseMotionListener(mouseMotionListener);
        editor.getCaretModel().addCaretListener(caretListener);
        // Todo - Implement
        // editor.getSelectionModel.addSelectionListener(selectionListener)
    }

    /**
     * Removes all the listeners
     */
    public void removeListeners() {
        editor.getDocument().removeDocumentListener(documentListener);
        editor.removeEditorMouseListener(mouseListener);
        editor.removeEditorMouseMotionListener(mouseMotionListener);
        editor.getCaretModel().removeCaretListener(caretListener);
        // Todo - Implement
        // editor.getSelectionModel.removeSelectionListener(selectionListener)
    }

    /**
     * Notifies the server that the corresponding document has been closed
     */
    public void documentClosed() {
        pool(() -> {
            if (this.isOpen) {
                requestManager.didClose(new DidCloseTextDocumentParams(identifier));
                isOpen = false;
                EditorEventManagerBase.editorToManager.remove(editor);
                EditorEventManagerBase.uriToManager.remove(FileUtils.editorToURIString(editor));
            } else {
                LOG.warn("Editor " + identifier.getUri() + " was already closed");
            }
        });
    }

    public void documentOpened() {
        pool(() -> {
            if (editor.isDisposed()) {
                return;
            }
            if (isOpen) {
                LOG.warn("Editor " + editor + " was already open");
            } else {
                final String extension = FileDocumentManager.getInstance().getFile(editor.getDocument()).getExtension();
                requestManager.didOpen(new DidOpenTextDocumentParams(new TextDocumentItem(identifier.getUri(),
                        wrapper.serverDefinition.languageIdFor(extension),
                        version++,
                        editor.getDocument().getText())));
                isOpen = true;
            }
        });
    }

    public void documentChanged(DocumentEvent event) {
        if (editor.isDisposed()) {
            return;
        }
        if (event.getDocument() == editor.getDocument()) {
            //Todo - restore when adding hover support
            // long predTime = System.nanoTime(); //So that there are no hover events while typing
            changesParams.getTextDocument().setVersion(version++);

            if (syncKind == TextDocumentSyncKind.Incremental) {
                TextDocumentContentChangeEvent changeEvent = changesParams.getContentChanges().get(0);
                CharSequence newText = event.getNewFragment();
                int offset = event.getOffset();
                int newTextLength = event.getNewLength();
                Position lspPosition = DocumentUtils.offsetToLSPPos(editor, offset);
                int startLine = lspPosition.getLine();
                int startColumn = lspPosition.getCharacter();
                CharSequence oldText = event.getOldFragment();

                //if text was deleted/replaced, calculate the end position of inserted/deleted text
                int endLine, endColumn;
                if (oldText.length() > 0) {
                    endLine = startLine + StringUtil.countNewLines(oldText);
                    String[] oldLines = oldText.toString().split("\n");
                    int oldTextLength = oldLines.length == 0 ? 0 : oldLines[oldLines.length - 1].length();
                    endColumn = oldLines.length == 1 ? startColumn + oldTextLength : oldTextLength;
                } else { //if insert or no text change, the end position is the same
                    endLine = startLine;
                    endColumn = startColumn;
                }
                Range range = new Range(new Position(startLine, startColumn), new Position(endLine, endColumn));
                changeEvent.setRange(range);
                changeEvent.setRangeLength(newTextLength);
                changeEvent.setText(newText.toString());
            } else if (syncKind == 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 saved
     */
    public void documentSaved() {
        pool(() -> {
            if (!editor.isDisposed()) {
                DidSaveTextDocumentParams params = new DidSaveTextDocumentParams(identifier, editor.getDocument().getText());
                requestManager.didSave(params);
            }
        });
    }

    /**
     * Indicates that the document will be saved
     */
    //TODO Manual
    public void willSave() {
        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 alternate between true or false, so the document will always be saved)
     */
    private void willSaveWaitUntil() {
        if (wrapper.isWillSaveWaitUntil()) {
            pool(() -> {
                if (editor.isDisposed()) {
                    return;
                }
                WillSaveTextDocumentParams params = new WillSaveTextDocumentParams(identifier,
                        TextDocumentSaveReason.Manual);
                CompletableFuture<List<TextEdit>> future = requestManager.willSaveWaitUntil(params);
                if (future != null) {
                    try {
                        List<TextEdit> edits = future.get(getTimeout(WILLSAVE), TimeUnit.MILLISECONDS);
                        wrapper.notifySuccess(Timeouts.WILLSAVE);
                        if (edits != null) {
                            invokeLater(() -> applyEdit(edits, "WaitUntil edits", false));
                        }
                    } catch (TimeoutException e) {
                        LOG.warn(e);
                        wrapper.notifyFailure(Timeouts.WILLSAVE);
                    } catch (JsonRpcException | ExecutionException | InterruptedException e) {
                        LOG.warn(e);
                        wrapper.crashed(e);
                    } finally {
                        needSave = true;
                        saveDocument();
                    }
                } else {
                    needSave = true;
                    saveDocument();
                }
            });
        } else {
            LOG.error("Server doesn't support WillSaveWaitUntil");
            needSave = true;
            saveDocument();
        }
    }

    // Tries to go to definition / show usages based on the element which is
    private void trySourceNavigationAndHover(EditorMouseEvent e) {
        if (editor.isDisposed()) {
            return;
        }

        createCtrlRange(DocumentUtils.logicalToLSPPos(editor.xyToLogicalPosition(e.getMouseEvent().getPoint()), editor),
                null);
        final CtrlRangeMarker ctrlRange = getCtrlRange();

        if (ctrlRange == null) {
            int offset = editor.logicalPositionToOffset(editor.xyToLogicalPosition(e.getMouseEvent().getPoint()));
            LSPReferencesAction referencesAction = (LSPReferencesAction) ActionManager.getInstance()
                    .getAction("LSPFindUsages");
            if (referencesAction != null) {
                referencesAction.forManagerAndOffset(this, offset);
            }
            return;
        }

        Location loc = ctrlRange.location;
        invokeLater(() -> {
            if (editor.isDisposed()) {
                return;
            }

            int offset = editor.logicalPositionToOffset(editor.xyToLogicalPosition(e.getMouseEvent().getPoint()));
            String locUri = FileUtils.sanitizeURI(loc.getUri());

            if (identifier.getUri().equals(locUri)
                    && offset >= DocumentUtils.LSPPosToOffset(editor, loc.getRange().getStart())
                    && offset <= DocumentUtils.LSPPosToOffset(editor, loc.getRange().getEnd())) {
                LSPReferencesAction referencesAction = (LSPReferencesAction) ActionManager.getInstance()
                        .getAction("LSPFindUsages");
                if (referencesAction != null) {
                    referencesAction.forManagerAndOffset(this, offset);
                }
            } else {
                VirtualFile file = null;
                try {
                    file = VfsUtil.findFileByURL(new URL(VfsUtilCore.fixURLforIDEA(locUri)));
                } catch (MalformedURLException e1) {
                    LOG.warn("Syntax Exception occurred for uri: " + locUri);
                }
                if (file != null) {
                    OpenFileDescriptor descriptor = new OpenFileDescriptor(project, file);
                    VirtualFile finalFile = file;
                    writeAction(() -> {
                        FileEditorManager.getInstance(project).openTextEditor(descriptor, true);
                        Editor srcEditor = FileUtils.editorFromVirtualFile(finalFile, project);
                        if (srcEditor != null) {
                            Position start = loc.getRange().getStart();
                            LogicalPosition logicalPos = DocumentUtils.getTabsAwarePosition(srcEditor, start);
                            if (logicalPos != null) {
                                srcEditor.getCaretModel().moveToLogicalPosition(logicalPos);
                                srcEditor.getScrollingModel().scrollTo(logicalPos, ScrollType.CENTER);
                            }
                        }
                    });
                } else {
                    LOG.warn("Empty file for " + locUri);
                }
            }

            ctrlRange.dispose();
            setCtrlRange(null);
        });
    }

    public void requestAndShowCodeActions() {
        invokeLater(() -> {
            if (editor.isDisposed()) {
                return;
            }
            if (annotations == null) {
                annotations = new ArrayList<>();
            }

            // sends code action request.
            int caretPos = editor.getCaretModel().getCurrentCaret().getOffset();
            List<Either<Command, CodeAction>> codeActionResp = codeAction(caretPos);
            if (codeActionResp == null || codeActionResp.isEmpty()) {
                return;
            }

            codeActionResp.forEach(element -> {
                if (element == null) {
                    return;
                }
                if (element.isLeft()) {
                    Command command = element.getLeft();
                    annotations.forEach(annotation -> {
                        int start = annotation.getStartOffset();
                        int end = annotation.getEndOffset();
                        if (start <= caretPos && end >= caretPos) {
                            annotation.registerFix(new LSPCommandFix(FileUtils.editorToURIString(editor), command),
                                    new TextRange(start, end));
                            codeActionSyncRequired = true;
                        }
                    });
                } else if (element.isRight()) {
                    CodeAction codeAction = element.getRight();
                    List<Diagnostic> diagnosticContext = codeAction.getDiagnostics();
                    annotations.forEach(annotation -> {
                        int start = annotation.getStartOffset();
                        int end = annotation.getEndOffset();
                        if (start <= caretPos && end >= caretPos) {
                            annotation.registerFix(new LSPCodeActionFix(FileUtils.editorToURIString(editor),
                                    codeAction), new TextRange(start, end));
                            codeActionSyncRequired = true;
                        }
                    });

                    // If the code actions does not have a diagnostics context, creates an intention action for
                    // the current line.
                    if ((diagnosticContext == null || diagnosticContext.isEmpty()) && anonHolder != null && !codeActionSyncRequired) {
                        // Calculates text range of the current line.
                        int line = editor.getCaretModel().getCurrentCaret().getLogicalPosition().line;
                        int startOffset = editor.getDocument().getLineStartOffset(line);
                        int endOffset = editor.getDocument().getLineEndOffset(line);
                        TextRange range = new TextRange(startOffset, endOffset);

                        Annotation annotation = this.anonHolder.createInfoAnnotation(range, codeAction.getTitle());
                        annotation.registerFix(new LSPCodeActionFix(FileUtils.editorToURIString(editor), codeAction), range);
                        this.annotations.add(annotation);
                        diagnosticSyncRequired = true;
                    }
                }
            });
            // If code actions are updated, forcefully triggers the inspection tool.
            if (codeActionSyncRequired) {
                updateErrorAnnotations();
            }
        });
    }

    /**
     * Triggers force full DaemonCodeAnalyzer execution.
     */
    private void updateErrorAnnotations() {
        computableReadAction(() -> {
            final PsiFile file = PsiDocumentManager.getInstance(project)
                    .getCachedPsiFile(editor.getDocument());
            if (file == null) {
                return null;
            }
            LOG.debug("Triggering force full DaemonCodeAnalyzer execution.");
            DaemonCodeAnalyzer.getInstance(project).restart(file);
            return null;
        });
    }

    private static class LSPTextEdit implements Comparable<LSPTextEdit> {
        private String text;
        private int startOffset;
        private int endOffset;

        LSPTextEdit(String text, int start, int end) {
            this.text = text;
            this.startOffset = start;
            this.endOffset = end;
        }

        String getText() {
            return text;
        }

        int getStartOffset() {
            return startOffset;
        }

        int getEndOffset() {
            return endOffset;
        }

        @Override
        public int compareTo(@NotNull LSPTextEdit te) {
            return te.getStartOffset() - getStartOffset();
        }
    }

    static class SnippetVariable {
        String lspSnippetText;
        int startIndex;
        int endIndex;
        String variableValue;
        String intellijSnippetText;

        SnippetVariable(String text, int start, int end) {
            this.lspSnippetText = text;
            this.startIndex = start;
            this.endIndex = end;
            this.variableValue = getVariableValue(text);
        }

        private String getVariableValue(String lspVarSnippet) {
            if (lspVarSnippet.contains(":")) {
                return lspVarSnippet.substring(lspVarSnippet.indexOf(':') + 1, lspVarSnippet.lastIndexOf('}'));
            }
            return " ";
        }
    }
}