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

com.github._1c_syntax.bsl.languageserver.hover.MethodSymbolMarkupContentBuilder Maven / Gradle / Ivy

Go to download

Language Server Protocol implementation for 1C (BSL) - 1C:Enterprise 8 and OneScript languages.

There is a newer version: 0.24.0-rc.1
Show newest version
/*
 * This file is a part of BSL Language Server.
 *
 * Copyright (c) 2018-2024
 * Alexey Sosnoviy , Nikita Fedkin  and contributors
 *
 * SPDX-License-Identifier: LGPL-3.0-or-later
 *
 * BSL Language Server 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.0 of the License, or (at your option) any later version.
 *
 * BSL Language Server 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 BSL Language Server.
 */
package com.github._1c_syntax.bsl.languageserver.hover;

import com.github._1c_syntax.bsl.languageserver.configuration.LanguageServerConfiguration;
import com.github._1c_syntax.bsl.languageserver.context.symbol.MethodSymbol;
import com.github._1c_syntax.bsl.languageserver.context.symbol.ParameterDefinition;
import com.github._1c_syntax.bsl.languageserver.context.symbol.description.MethodDescription;
import com.github._1c_syntax.bsl.languageserver.context.symbol.description.ParameterDescription;
import com.github._1c_syntax.bsl.languageserver.context.symbol.description.TypeDescription;
import com.github._1c_syntax.bsl.languageserver.utils.MdoRefBuilder;
import com.github._1c_syntax.bsl.languageserver.utils.Resources;
import lombok.RequiredArgsConstructor;
import org.eclipse.lsp4j.MarkupContent;
import org.eclipse.lsp4j.MarkupKind;
import org.eclipse.lsp4j.SymbolKind;
import org.springframework.stereotype.Component;

import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.StringJoiner;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * Построитель контента для всплывающего окна для {@link MethodSymbol}.
 */
@Component
@RequiredArgsConstructor
public class MethodSymbolMarkupContentBuilder implements MarkupContentBuilder {

  private static final String PROCEDURE_KEY = "procedure";
  private static final String FUNCTION_KEY = "function";
  private static final String EXPORT_KEY = "export";
  private static final String VAL_KEY = "val";
  private static final String PARAMETERS_KEY = "parameters";
  private static final String RETURNED_VALUE_KEY = "returnedValue";
  private static final String EXAMPLES_KEY = "examples";
  private static final String CALL_OPTIONS_KEY = "callOptions";
  private static final String PARAMETER_TEMPLATE = "* **%s**: %s";

  private final LanguageServerConfiguration configuration;

  @Override
  public MarkupContent getContent(MethodSymbol symbol) {
    var markupBuilder = new StringJoiner("\n");

    // сигнатура
    // местоположение метода
    // описание метода
    // параметры
    // возвращаемое значение
    // примеры
    // варианты вызова

    // сигнатура
    String signature = getSignature(symbol);
    addSectionIfNotEmpty(markupBuilder, signature);

    // местоположение метода
    String methodLocation = getLocation(symbol);
    addSectionIfNotEmpty(markupBuilder, methodLocation);

    // описание метода
    String purposeSection = getPurposeSection(symbol);
    addSectionIfNotEmpty(markupBuilder, purposeSection);

    // параметры
    String parametersSection = getParametersSection(symbol);
    addSectionIfNotEmpty(markupBuilder, parametersSection);

    // возвращаемое значение
    String returnedValueSection = getReturnedValueSection(symbol);
    addSectionIfNotEmpty(markupBuilder, returnedValueSection);

    // примеры
    String examplesSection = getExamplesSection(symbol);
    addSectionIfNotEmpty(markupBuilder, examplesSection);

    // варианты вызова
    String callOptionsSection = getCallOptionsSection(symbol);
    addSectionIfNotEmpty(markupBuilder, callOptionsSection);

    String content = markupBuilder.toString();

    return new MarkupContent(MarkupKind.MARKDOWN, content);
  }

  @Override
  public SymbolKind getSymbolKind() {
    return SymbolKind.Method;
  }

  private static void addSectionIfNotEmpty(StringJoiner markupBuilder, String newContent) {
    if (!newContent.isEmpty()) {
      markupBuilder.add(newContent);
      markupBuilder.add("");
      markupBuilder.add("---");
    }
  }

  private static String getPurposeSection(MethodSymbol methodSymbol) {
    return methodSymbol.getDescription()
      .map(MethodDescription::getPurposeDescription)
      .orElse("");
  }

  private String getParametersSection(MethodSymbol methodSymbol) {
    var result = new StringJoiner("  \n"); // два пробела
    methodSymbol.getParameters().forEach(parameterDefinition ->
      result.add(parameterToString(parameterDefinition))
    );

    var parameters = result.toString();

    if (!parameters.isBlank()) {
      var parametersSection = new StringJoiner("\n");
      String header = "**" + getResourceString(PARAMETERS_KEY) + ":**";
      parametersSection.add(header);
      parametersSection.add("");
      parametersSection.add(parameters);
      return parametersSection.toString();
    }

    return "";
  }

  private String getReturnedValueSection(MethodSymbol methodSymbol) {
    var result = new StringJoiner("  \n"); // два пробела
    methodSymbol.getDescription().ifPresent((MethodDescription methodDescription) -> {
      Map typesMap = typesToMap(methodDescription.getReturnedValue(), 0);
      result.add(typesMapToString(typesMap, 1));
    });

    var returnedValue = result.toString();

    if (!returnedValue.isEmpty()) {
      returnedValue = "**" + getResourceString(RETURNED_VALUE_KEY) + ":**\n\n" + returnedValue;
    }

    return returnedValue;
  }

  private String getExamplesSection(MethodSymbol methodSymbol) {
    var examples = methodSymbol.getDescription()
      .map(MethodDescription::getExamples)
      .orElseGet(Collections::emptyList);
    return getSectionWithCodeFences(examples, EXAMPLES_KEY);
  }

  private String getCallOptionsSection(MethodSymbol methodSymbol) {
    var callOptions = methodSymbol.getDescription()
      .map(MethodDescription::getCallOptions)
      .orElseGet(Collections::emptyList);
    return getSectionWithCodeFences(callOptions, CALL_OPTIONS_KEY);
  }

  private String getSectionWithCodeFences(List codeBlocks, String resourceKey) {
    String codeFences = codeBlocks
      .stream()
      .map(codeBlock -> "```bsl\n" + codeBlock + "\n```")
      .collect(Collectors.joining("\n"));

    if (!codeFences.isEmpty()) {
      codeFences = "**" + getResourceString(resourceKey) + ":**\n\n" + codeFences;
    }

    return codeFences;
  }

  private static String getLocation(MethodSymbol symbol) {
    var documentContext = symbol.getOwner();
    var startPosition = symbol.getSelectionRange().getStart();
    String mdoRef = MdoRefBuilder.getMdoRef(documentContext);

    return String.format(
      "[%s](%s#%d)",
      mdoRef,
      documentContext.getUri(),
      startPosition.getLine() + 1
    );
  }

  private String getSignature(MethodSymbol methodSymbol) {
    String signatureTemplate = "```bsl\n%s %s(%s)%s%s\n```";

    String methodKind;
    if (methodSymbol.isFunction()) {
      methodKind = getResourceString(FUNCTION_KEY);
    } else {
      methodKind = getResourceString(PROCEDURE_KEY);
    }
    String methodName = methodSymbol.getName();

    var parametersDescription = new StringJoiner(", ");
    methodSymbol.getParameters().forEach((ParameterDefinition parameterDefinition) -> {
      var parameter = "";
      var parameterName = parameterDefinition.getName();

      if (parameterDefinition.isByValue()) {
        parameter = parameter + getResourceString(VAL_KEY) + " ";
      }
      parameter += parameterName;

      var parameterTypes = parameterDefinition.getDescription()
        .map(ParameterDescription::getTypes)
        .map(MethodSymbolMarkupContentBuilder::getTypes)
        .orElse("");

      if (!parameterTypes.isEmpty()) {
        parameter += ": " + parameterTypes;
      }

      if (parameterDefinition.isOptional()) {
        parameter += " = ";
        parameter += parameterDefinition.getDefaultValue().getValue();
      }

      parametersDescription.add(parameter);
    });
    var parameters = parametersDescription.toString();

    String returnedValueType = methodSymbol.getDescription()
      .map(MethodDescription::getReturnedValue)
      .map(MethodSymbolMarkupContentBuilder::getTypes)
      .orElse("");
    if (!returnedValueType.isEmpty()) {
      returnedValueType = ": " + returnedValueType;
    }

    String export = methodSymbol.isExport() ? (" " + getResourceString(EXPORT_KEY)) : "";

    return String.format(
      signatureTemplate,
      methodKind,
      methodName,
      parameters,
      export,
      returnedValueType
    );
  }

  private static String getTypes(List typeDescriptions) {
    return typeDescriptions.stream()
      .map(TypeDescription::getName)
      .flatMap(parameterType -> Stream.of(parameterType.split(",")))
      .map(String::trim)
      .collect(Collectors.joining(" | "));
  }

  private String getResourceString(String key) {
    return Resources.getResourceString(configuration.getLanguage(), getClass(), key);
  }

  public static String parameterToString(ParameterDescription parameter, int level) {
    var result = new StringJoiner("  \n"); // два пробела
    Map typesMap = typesToMap(parameter.getTypes(), level);
    var parameterTemplate = "  ".repeat(level) + PARAMETER_TEMPLATE;

    if (typesMap.size() == 1) {
      result.add(String.format(parameterTemplate,
        parameter.getName(),
        typesMapToString(typesMap, 0)));
    } else {
      result.add(String.format(parameterTemplate, parameter.getName(), ""));
      result.add(typesMapToString(typesMap, level + 1));
    }
    return result.toString();
  }

  public static String parameterToString(ParameterDefinition parameterDefinition) {
    int level = 0;
    if (parameterDefinition.getDescription().isPresent()) {
      return parameterToString(parameterDefinition.getDescription().get(), level);
    }

    return String.format(PARAMETER_TEMPLATE, parameterDefinition.getName(), "");
  }

  private static Map typesToMap(List parameterTypes, int level) {
    Map types = new HashMap<>();

    parameterTypes.forEach((TypeDescription type) -> {
      var typeDescription = typeToString(type, level);
      String typeName;
      if (type.isHyperlink()) {
        typeName = String.format("[%s](%s)", type.getName(), type.getLink());
      } else {
        typeName = String.format("`%s`", type.getName());
      }

      types.merge(typeDescription, typeName, (oldValue, newValue) -> String.format("%s | %s", oldValue, newValue));
    });
    return types;
  }

  private static String typesMapToString(Map types, int level) {
    var result = new StringJoiner("  \n"); // два пробела
    var indent = "  ".repeat(level);
    types.forEach((String key, String value) -> {
      if (key.isBlank()) {
        result.add(value);
      } else {
        result.add(String.format("%s%s %s", indent, value, key));
      }
    });
    return result.toString();
  }

  private static String typeToString(TypeDescription type, int level) {
    var result = new StringJoiner("  \n"); // два пробела
    var description = type.getDescription().replace("\n", "
" + "  ".repeat(level + 1)); if (!description.isBlank()) { description = "- " + description; } if (!type.getParameters().isEmpty()) { description += ":"; } result.add(description); type.getParameters().forEach((ParameterDescription parameter) -> result.add(parameterToString(parameter, level + 1))); return result.toString(); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy