All Downloads are FREE. Search and download functionalities are using the official Maven repository.

fr.cenotelie.commons.lsp.engine.Workspace Maven / Gradle / Ivy

/*******************************************************************************
 * Copyright (c) 2017 Association Cénotélie (cenotelie.fr)
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation, either version 3
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General
 * Public License along with this program.
 * If not, see .
 ******************************************************************************/

package fr.cenotelie.commons.lsp.engine;

import fr.cenotelie.commons.jsonrpc.JsonRpcRequest;
import fr.cenotelie.commons.lsp.LspEndpointLocal;
import fr.cenotelie.commons.lsp.server.LspServer;
import fr.cenotelie.commons.lsp.structures.*;
import fr.cenotelie.commons.utils.IOUtils;
import fr.cenotelie.commons.utils.json.SerializedUnknown;
import fr.cenotelie.commons.utils.logging.Logging;

import java.io.File;
import java.io.IOException;
import java.io.Reader;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

/**
 * Represents the current workspace for a server
 *
 * @author Laurent Wouters
 */
public class Workspace {
    /**
     * The documents in the workspace
     */
    protected final Map documents;
    /**
     * The global symbol registry
     */
    protected final SymbolRegistry symbolRegistry;
    /**
     * The local LSP endpoint
     */
    protected LspEndpointLocal local;

    /**
     * Gets the server capabilities that are supported by this workspace
     *
     * @return The server capabilities that are supported by this workspace
     */
    public ServerCapabilities getServerCapabilities() {
        ServerCapabilities capabilities = new ServerCapabilities();
        capabilities.addCapability("textDocumentSync.openClose");
        capabilities.addCapability("textDocumentSync.willSave");
        capabilities.addCapability("textDocumentSync.willSaveWaitUntil");
        capabilities.addCapability("textDocumentSync.save.includeText");
        capabilities.addOption("textDocumentSync.change", TextDocumentSyncKind.INCREMENTAL);
        listServerCapabilities(capabilities);
        return capabilities;
    }

    /**
     * Gets the client capabilities that are supported by this workspace
     *
     * @return The client capabilities that are supported by this workspace
     */
    public ClientCapabilities getClientCapabilities() {
        ClientCapabilities capabilities = new ClientCapabilities();
        listClientCapabilities(capabilities);
        return capabilities;
    }

    /**
     * Gets the documents in the workspace
     *
     * @return The documents in the workspace
     */
    public Collection getDocuments() {
        return documents.values();
    }

    /**
     * Gets the document for the specified URI
     *
     * @param uri The URI of a document
     * @return The document, or null if it does not exist
     */
    public Document getDocument(String uri) {
        return documents.get(uri);
    }

    /**
     * Gets the associated symbol registry
     *
     * @return The associated symbol registry
     */
    public SymbolRegistry getSymbols() {
        return symbolRegistry;
    }

    /**
     * Gets the local LSP endpoint
     *
     * @return The local LSP endpoint
     */
    public LspEndpointLocal getLocal() {
        return local;
    }

    /**
     * Sets the local LSP endpoint
     *
     * @param local The local LSP endpoint
     */
    public void setLocal(LspEndpointLocal local) {
        this.local = local;
    }

    /**
     * Initializes an empty workspace
     */
    public Workspace() {
        this.documents = new HashMap<>();
        this.symbolRegistry = new SymbolRegistry();
    }

    /**
     * Initializes this workspace
     *
     * @param rootUri  The root uri for the workspace
     * @param rootPath The root path for the workspace
     */
    public void onInitWorkspace(String rootUri, String rootPath) {
        File workspaceRoot = null;
        if (rootUri != null && rootUri.startsWith("file://"))
            workspaceRoot = new File(rootUri.substring("file://".length()));
        else if (rootPath != null)
            workspaceRoot = new File(rootPath);
        if (workspaceRoot != null && workspaceRoot.exists())
            scanWorkspace(workspaceRoot);
    }

    /**
     * Scans the specified content
     *
     * @param file The current file or directory
     */
    private void scanWorkspace(File file) {
        if (isWorkspaceExcluded(file))
            return;
        if (file.isDirectory()) {
            File[] files = file.listFiles();
            if (files == null)
                return;
            for (int i = 0; i != files.length; i++)
                scanWorkspace(files[i]);
        } else {
            if (!isWorkspaceIncluded(file))
                return;
            Document document = resolveDocument(file);
            if (document == null)
                return;
            doDocumentAnalysis(document, false);
        }
    }

    /**
     * Determines whether the specified file or directory is excluded
     *
     * @param file The current file or directory
     * @return true if the file is excluded
     */
    protected boolean isWorkspaceExcluded(File file) {
        if (file.isDirectory()) {
            String name = file.getName();
            return (name.equals(".git") || name.equals(".hg") || name.equals(".svn"));
        }
        return false;
    }

    /**
     * Determines whether the specified file should be analyzed
     *
     * @param file The current file
     * @return true if the file should be analyzed
     */
    protected boolean isWorkspaceIncluded(File file) {
        return false;
    }

    /**
     * Resolves the document for the specified file
     *
     * @param file The file
     * @return The document
     */
    protected Document resolveDocument(File file) {
        String uri = "file://" + file.getAbsolutePath();
        Document document = documents.get(uri);
        if (document == null) {
            try (Reader reader = IOUtils.getAutoReader(file)) {
                String content = IOUtils.read(reader);
                document = new Document(uri, getLanguageFor(file), 0, content);
                documents.put(uri, document);
            } catch (IOException exception) {
                Logging.get().error(exception);
            }
        }
        return document;
    }

    /**
     * Gets the language associated to the specified file
     *
     * @param file The file
     * @return The associated language
     */
    protected String getLanguageFor(File file) {
        return "text";
    }

    /**
     * When file events have been received
     *
     * @param events The received file events
     */
    public void onFileEvents(FileEvent[] events) {
        for (int i = 0; i != events.length; i++) {
            onFileEvent(events[i]);
        }
    }

    /**
     * When file event have been received
     *
     * @param event The received file event
     */
    private void onFileEvent(FileEvent event) {
        switch (event.getType()) {
            case FileChangeType.DELETED: {
                Document document = documents.get(event.getUri());
                if (document == null)
                    return;
                documents.remove(document.getUri());
                symbolRegistry.onDocumentRemoved(document);
                break;
            }
            case FileChangeType.CREATED: {
                if (!event.getUri().startsWith("file://"))
                    return;
                File file = new File(event.getUri().substring("file://".length()));
                if (!file.exists() || isWorkspaceExcluded(file) || !isWorkspaceIncluded(file))
                    return;
                Document document = resolveDocument(file);
                if (document == null)
                    return;
                doDocumentAnalysis(document, true);
                break;
            }
            case FileChangeType.CHANGED: {
                Document document = documents.get(event.getUri());
                if (document == null)
                    return;
                doDocumentAnalysis(document, true);
                break;
            }
        }
    }

    /**
     * When a text document has been open
     *
     * @param documentItem The document item
     */
    public void onDocumentOpen(TextDocumentItem documentItem) {
        Document document = new Document(
                documentItem.getUri(),
                documentItem.getLanguageId(),
                documentItem.getVersion(),
                documentItem.getText());
        documents.put(documentItem.getUri(), document);
        doDocumentAnalysis(document, true);
    }

    /**
     * When document changes occurred on the client
     *
     * @param textDocument   The document that did change
     * @param contentChanges The actual content changes
     */
    public void onDocumentChange(VersionedTextDocumentIdentifier textDocument, TextDocumentContentChangeEvent[] contentChanges) {
        Document document = documents.get(textDocument.getUri());
        if (document != null) {
            document.mutateTo(textDocument.getVersion(), contentChanges);
            doDocumentAnalysis(document, true);
        }
    }

    /**
     * When a document is being saved
     *
     * @param textDocument The document that was is being saved
     * @param reason       The reason for the save
     */
    public void onDocumentWillSave(TextDocumentIdentifier textDocument, int reason) {
        // do nothing
    }

    /**
     * When a document is being saved
     *
     * @param textDocument The document that was is being saved
     * @param reason       The reason for the save
     */
    public TextEdit[] onDocumentWillSaveUntil(TextDocumentIdentifier textDocument, int reason) {
        // do nothing
        return null;
    }

    /**
     * When a document has been saved on the client
     *
     * @param textDocument The document that was saved
     * @param text         The full text for the saved document, if available
     */
    public void onDocumentDidSave(TextDocumentIdentifier textDocument, String text) {
        if (text != null) {
            Document document = documents.get(textDocument.getUri());
            if (document != null) {
                document.setFullContent(text);
                doDocumentAnalysis(document, true);
            }
        }
    }

    /**
     * When a document has been closed on the client
     *
     * @param textDocument The document that was closed
     */
    public void onDocumentDidClose(TextDocumentIdentifier textDocument) {
        // do nothing
    }

    /**
     * Performs the analysis of a document
     *
     * @param document           The document
     * @param publishDiagnostics Whether to publish the diagnostics
     */
    protected void doDocumentAnalysis(Document document, boolean publishDiagnostics) {
        DocumentAnalyzer analyzer = getServiceAnalyzer(document);
        if (analyzer == null)
            return;
        DocumentAnalysis analysis = analyzer.analyze(symbolRegistry, document);
        document.setLastAnalysis(analysis);
        if (local != null && publishDiagnostics) {
            local.send(new JsonRpcRequest(
                    null,
                    "textDocument/publishDiagnostics",
                    new PublishDiagnosticsParams(document.getUri(), analysis.getDiagnostics().toArray(new Diagnostic[analysis.getDiagnostics().size()]))
            ));
        }
        symbolRegistry.onDocumentChanged(document, analysis.getSymbols());
    }

    /**
     * Executes a command on this workspace
     *
     * @param parameters The parameters for this request
     * @return The result of the command execution
     */
    public Object executeCommand(ExecuteCommandParams parameters) {
        return null;
    }

    /**
     * Gets the completion items for the specified document and position
     *
     * @param parameters The text document parameters
     * @return The list of completion items
     */
    public CompletionList getCompletion(TextDocumentPositionParams parameters) {
        Document document = documents.get(parameters.getTextDocument().getUri());
        if (document == null)
            return new CompletionList(false, new CompletionItem[0]);
        DocumentCompleter completer = getServiceCompleter(document);
        if (completer == null)
            return new CompletionList(false, new CompletionItem[0]);
        CompletionList result = completer.getCompletionItems(document, parameters.getPosition());
        SerializedUnknown data = new SerializedUnknown();
        data.addProperty("documentUri", parameters.getTextDocument().getUri());
        data.addProperty("locationLine", parameters.getPosition().getLine());
        data.addProperty("locationChar", parameters.getPosition().getCharacter());
        for (CompletionItem item : result.getItems()) {
            item.setData(data);
        }
        return result;
    }

    /**
     * Resolves a completion item
     *
     * @param item The completion item to resolve
     * @return The resolved completion item
     */
    public CompletionItem resolveCompletion(CompletionItem item) {
        if (item.getData() == null)
            return item;
        SerializedUnknown data = (SerializedUnknown) item.getData();
        Document document = documents.get((String) data.getValueFor("documentUri"));
        if (document == null)
            return item;
        DocumentCompleter completer = getServiceCompleter(document);
        if (completer == null)
            return item;
        return completer.resolve(document, new Position((Integer) data.getValueFor("locationLine"), (Integer) data.getValueFor("locationChar")), item);
    }

    /**
     * Gets the hover data for the specified document and position
     *
     * @param parameters The text document parameters
     * @return The hover data
     */
    public Hover getHover(TextDocumentPositionParams parameters) {
        Document document = documents.get(parameters.getTextDocument().getUri());
        if (document == null)
            return new Hover();
        DocumentHoverProvider service = getServiceHoverProvider(document);
        if (service == null)
            return new Hover();
        return service.getHoverData(document, parameters.getPosition());
    }

    /**
     * Gets the signature help for the specified document and position
     *
     * @param parameters The text document parameters
     * @return The signature help
     */
    public SignatureHelp getSignatures(TextDocumentPositionParams parameters) {
        Document document = documents.get(parameters.getTextDocument().getUri());
        if (document == null)
            return new SignatureHelp(new SignatureInformation[0]);
        DocumentSignatureHelper service = getServiceSignatureHelp(document);
        if (service == null)
            return new SignatureHelp(new SignatureInformation[0]);
        return service.getSignatures(document, parameters.getPosition());
    }

    /**
     * Formats a whole document
     *
     * @param parameters The parameters for this request
     * @return The edits representing the formatting result
     */
    public TextEdit[] formatDocument(DocumentFormattingParams parameters) {
        Document document = documents.get(parameters.getTextDocument().getUri());
        if (document == null)
            return new TextEdit[0];
        DocumentFormatter service = getServiceFormatter(document);
        if (service == null)
            return new TextEdit[0];
        return service.format(parameters.getOptions(), document);
    }

    /**
     * Formats a range in document
     *
     * @param parameters The parameters for this request
     * @return The edits representing the formatting result
     */
    public TextEdit[] formatRange(DocumentRangeFormattingParams parameters) {
        Document document = documents.get(parameters.getTextDocument().getUri());
        if (document == null)
            return new TextEdit[0];
        DocumentFormatter service = getServiceFormatter(document);
        if (service == null)
            return new TextEdit[0];
        return service.format(parameters.getOptions(), document, parameters.getRange());
    }

    /**
     * Formats a part of the document when a character has been typed
     *
     * @param parameters The parameters for this request
     * @return The edits representing the formatting result
     */
    public TextEdit[] formatOnTyped(DocumentOnTypeFormattingParams parameters) {
        Document document = documents.get(parameters.getTextDocument().getUri());
        if (document == null)
            return new TextEdit[0];
        DocumentFormatter service = getServiceFormatter(document);
        if (service == null)
            return new TextEdit[0];
        return service.format(parameters.getOptions(), document, parameters.getPosition(), parameters.getCharacter());
    }

    /**
     * Gets available code actions for the specified parameters
     *
     * @param parameters The parameters for this request
     * @return The available code actions
     */
    public Command[] getCodeActions(CodeActionParams parameters) {
        Document document = documents.get(parameters.getTextDocument().getUri());
        if (document == null)
            return new Command[0];
        DocumentActionProvider service = getServiceActionProvider(document);
        if (service == null)
            return new Command[0];
        return service.getActions(document, parameters.getRange(), parameters.getContext());
    }

    /**
     * Gets the code lens for the specified parameters
     *
     * @param parameters The parameters for this request
     * @return The available code lens
     */
    public CodeLens[] getCodeLens(CodeLensParams parameters) {
        Document document = documents.get(parameters.getTextDocument().getUri());
        if (document == null)
            return new CodeLens[0];
        DocumentLensProvider service = getServiceLensProvider(document);
        if (service == null)
            return new CodeLens[0];
        CodeLens[] lenses = service.getLens(document);
        String data = parameters.getTextDocument().getUri();
        for (CodeLens lens : lenses)
            lens.setData(data);
        return lenses;
    }

    /**
     * Resolves a code lens
     *
     * @param lens The code lens to resolve
     * @return The resolved code lens
     */
    public CodeLens resolveCodeLens(CodeLens lens) {
        if (lens.getData() == null)
            return lens;
        Document document = documents.get((String) lens.getData());
        if (document == null)
            return lens;
        DocumentLensProvider service = getServiceLensProvider(document);
        if (service == null)
            return lens;
        return service.resolve(lens);
    }

    /**
     * Gets the document links for the specified parameters
     *
     * @param parameters The parameters for this request
     * @return The document links
     */
    public DocumentLink[] getDocumentLinks(DocumentLinkParams parameters) {
        Document document = documents.get(parameters.getTextDocument().getUri());
        if (document == null)
            return new DocumentLink[0];
        DocumentAnalysis analysis = document.getLastAnalysis();
        if (analysis == null)
            return new DocumentLink[0];
        return analysis.getLinks().toArray(new DocumentLink[analysis.getLinks().size()]);
    }

    /**
     * Resolves a document link
     *
     * @param link The document link to resolve
     * @return The resolved document link
     */
    public DocumentLink resolveDocumentLink(DocumentLink link) {
        return link;
    }

    /**
     * Renames all occurrences of a symbol within this workspace
     *
     * @param parameters The parameters for the operation
     * @return The operation's result as a list of edits to be applied to documents in this workspace
     */
    public WorkspaceEdit renameSymbol(RenameParams parameters) {
        Document document = documents.get(parameters.getTextDocument().getUri());
        if (document == null)
            return null;
        Symbol symbol = symbolRegistry.getSymbolAt(parameters.getTextDocument().getUri(), parameters.getPosition());
        if (symbol == null)
            return null;
        DocumentSymbolHandler service = getServiceSymbolHandler(document);
        if (service == null)
            return null;
        if (!service.isLegalName(document, symbolRegistry, symbol, parameters.getNewName()))
            return null;
        Map edits = new HashMap<>();
        for (String uri : symbol.getDefiningDocuments()) {
            Document doc = documents.get(uri);
            if (doc == null)
                return null;
            service = getServiceSymbolHandler(doc);
            if (service == null)
                return null;
            TextEdit[] changes = service.rename(doc, symbol, parameters.getNewName());
            if (changes == null)
                return null;
            if (changes.length > 0)
                edits.put(doc, changes);
        }
        for (String uri : symbol.getReferencingDocuments()) {
            if (symbol.getDefiningDocuments().contains(uri))
                continue;
            Document doc = documents.get(uri);
            if (doc == null)
                return null;
            service = getServiceSymbolHandler(doc);
            if (service == null)
                return null;
            TextEdit[] changes = service.rename(doc, symbol, parameters.getNewName());
            if (changes == null)
                return null;
            if (changes.length > 0)
                edits.put(doc, changes);
        }

        WorkspaceEdit result = new WorkspaceEdit();
        if (local != null && local instanceof LspServer && ((LspServer) local).getClientCapabiltiies().supports("workspace.workspaceEdit.documentChanges")) {
            for (Map.Entry entry : edits.entrySet()) {
                result.addChanges(entry.getKey().getUri(), entry.getKey().getCurrentVersion().getNumber(), entry.getValue());
            }
        } else {
            for (Map.Entry entry : edits.entrySet()) {
                result.addChanges(entry.getKey().getUri(), entry.getValue());
            }
        }
        return result;
    }

    /**
     * Lists the server capabilities that are supported by services provided by this entity
     *
     * @param capabilities The server capabilities structure to fill
     */
    protected void listServerCapabilities(ServerCapabilities capabilities) {
        // do nothing
    }

    /**
     * Lists the client capabilities that are supported by services provided by this entity
     *
     * @param capabilities The client capabilities structure to fill
     */
    protected void listClientCapabilities(ClientCapabilities capabilities) {
        // do nothing
    }

    /**
     * Gets the document analyzer service for the specified document
     *
     * @param document A document
     * @return The corresponding document analyzer
     */
    protected DocumentAnalyzer getServiceAnalyzer(Document document) {
        return null;
    }

    /**
     * Gets the document completer service for the specified document
     *
     * @param document A document
     * @return The corresponding document completer
     */
    protected DocumentCompleter getServiceCompleter(Document document) {
        return null;
    }

    /**
     * Gets the document hover provider service for the specified document
     *
     * @param document A document
     * @return The corresponding document hover provider
     */
    protected DocumentHoverProvider getServiceHoverProvider(Document document) {
        return null;
    }

    /**
     * Gets the document signature helper for the specified document
     *
     * @param document A document
     * @return The corresponding document signature helper
     */
    protected DocumentSignatureHelper getServiceSignatureHelp(Document document) {
        return null;
    }

    /**
     * Gets the document formatter for the specified document
     *
     * @param document A document
     * @return The corresponding document formatter
     */
    protected DocumentFormatter getServiceFormatter(Document document) {
        return null;
    }

    /**
     * Gets the document action provider for the specified document
     *
     * @param document A document
     * @return The corresponding document action provider
     */
    protected DocumentActionProvider getServiceActionProvider(Document document) {
        return null;
    }

    /**
     * Gets the document lens provider for the specified document
     *
     * @param document A document
     * @return The corresponding document lens provider
     */
    protected DocumentLensProvider getServiceLensProvider(Document document) {
        return null;
    }

    /**
     * Gets the document symbol handler for the specified document
     *
     * @param document A document
     * @return The corresponding document symbol handler
     */
    protected DocumentSymbolHandler getServiceSymbolHandler(Document document) {
        return null;
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy