© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2022
N. Gunasinghe, N. MarcusLanguage Server Protocol and Implementationhttps://doi.org/10.1007/978-1-4842-7792-8_7

7. Refactoring and Code Fixes

Nadeeshaan Gunasinghe1   and Nipuna Marcus2
(1)
Walahanduwa, Sri Lanka
(2)
Mawathagama, Sri Lanka
 

In the previous chapter, we had a look at language features which are used to provide smart editing, documentation support, and diagnostics. When we consider language intelligence, another most useful set of features are refactoring features and code fixes. For example, most of the time the user tends to change variable names, adhere to formatting guidelines, and fix linter issues during the editing process. In this chapter, we are going to look at a set of language features exposed by the Language Server Protocol to achieve refactoring and code fixing capabilities.

Rename

The textDocument/rename request is sent from the client to the server to rename a given token/symbol. For example, the user tries to rename a function name/type name. As we are going to look at the next subsection, the textDocument/prepareRename can be considered as the prevalidation request for the rename operation. When the user requests a rename over a token, it is the server which has the semantic knowledge to determine whether the particular token can be renamed or not. For example, programming languages have reserved keywords, and the user can mistakenly request a rename for a keyword. In such scenarios, the server can make use of the prepareRename support and terminate the operation with a notification. Also, programming languages, such as the Ballerina language, allow the user to use reserved tokens as variable names by escaping them with a single quote (int 'public = 10; // public is a keyword). If we consider the last scenario, the user can rename a token to a keyword. Then as a resulting refactor, the server can rename the token to a keyword with a single quote as a prefix. We will be looking at handling this scenario in a more user-friendly manner with the AnnotatedTextEdits later in this section.

Initialization and Capabilities

Client Capabilities

Client capabilities are specified with RenameClientCapabilities with the following properties.

The dynamicRegistration property specifies whether the client supports dynamic registration of the rename operation.

As mentioned earlier, the rename operation has a supportive operation as prepareRename, and the client specifies whether it supports the prepareRename capability by setting the prepareSupport property.

The prepareSupportDefaultBehaviour specifies the default behavior of the client on selection. The current specification only supports the Identifier selection according to the syntax rules. We will be discussing this in detail later in this section.

The honorsChangeAnnotations specifies whether the rename feature supports change annotations associated with the annotated text edits. For the rename’s result, the server can prepare the WorkspaceEdit either with a list of TextEdit models or a list of AnnotatedTextEdits models.

Server Capabilities

Server capabilities are specified with RenameOptions. The server specifies whether it supports the prepareRename operation by setting the prepareProvider property. The client will honor this, only if the client also can support the prepareRename operation.

Listing 7-1 shows how to specify the server capabilities at the initialization of the Language Server.
public static RenameOptions getRenameOptions() {
    RenameOptions renameOptions = new RenameOptions();
    // Set the prepare support from the server
    renameOptions.setPrepareProvider(true);
    return renameOptions;
}
Listing 7-1

Set Rename Options (ServerInitUtils.java)

Generating the Workspace Edit

The server responds to the textDocument/rename request with a WorkspaceEdit which applies to the whole workspace. The RenameParams sent with the rename request includes the new name to be used for the renaming which is specified with the newName property. If the new name to be included is valid, then we capture all the references of the particular symbol and generate the workspace edit.

When generating the workspace edit, the server should be aware of the client capabilities such as documentChanges support and changeAnnotationSupport. In Listing 7-2, we use document changes instead of plain changes as well as demonstrate the changeAnnotations. The listing shows how we compute document changes, and for simplicity we have excluded the Ballerina compiler–specific code segments; you can refer to RenameProvider.java for the complete example.

In our example, you can see, when we are looping through the references we found, we check whether our new name is a keyword or not. We mentioned earlier in this section that Ballerina allows the use of keywords as identifiers by escaping them with a single quote. Now, let’s say the user is renaming a variable to a keyword:
int varName = 123; // user rename varName to int
In this scenario, the server can append a single quote to the new name and insert. Although it is not the same value the user expected as the new name, it is a better developer experience to get the user’s confirmation before applying the rename changes. We can make use of the AnnotatedTextEdit support to facilitate this requirement. We have defined two ChangeAnnotations as an enum representation for unquoted and quoted edits as shown in Listing 7-3. As shown in Listing 7-2, we use these defined annotations to generate two annotated text edits for a given reference. When the user invokes the rename operation, then the client will show the rename options before automatically applying them, as shown in Figure 7-1. This is one usage of the annotated text edits, and the server can use this capability for use cases such as code actions as well.
../images/509925_1_En_7_Chapter/509925_1_En_7_Fig1_HTML.jpg
Figure 7-1

Annotated text edits for renaming a variable

If we have a look at our rename document change generation, for the nonkeyword scenarios, we only generate the TextEdits, and the changes will be applied on the source editor without the user’s consent.
private static List<Either<TextDocumentEdit, ResourceOperation>>
getDocumentChanges(BalRenameContext context, String newName) {
    List<Either<TextDocumentEdit, ResourceOperation>>
            textDocumentEdits = new ArrayList<>();
    ...
    Map<String, List<TextEdit>> textEditMap = new HashMap<>();
    for (Module module : modules) {
        ...
        List<Location> references = semanticModel.references(symbol.get());
        // Looping the reference and generate edits
        for (Location reference : references) {
            Range range = toRange(reference.lineRange());
            List<TextEdit> textEdits = new ArrayList<>();
            if (CommonUtils.isKeyword(newName)) {
                /*
                If the new name is a keyword we add annotated edits
                for quoted and unquoted new names
                 */
                String quotedName = "'" + newName;
                TextEdit quoted = new AnnotatedTextEdit(range,
                        quotedName,
                        RenameChangeAnnotation.withQuote.getId());
                TextEdit plain = new AnnotatedTextEdit(range,
                        newName,
                        RenameChangeAnnotation.withoutQuote.getId());
                textEdits.add(quoted);
                textEdits.add(plain);
            } else {
                textEdits.add(new TextEdit(range, newName));
            }
            String uri = modulePath.resolve(reference.lineRange()
                    .filePath()).toUri().toString();
            if (textEditMap.containsKey(uri)) {
                textEditMap.get(uri).addAll(textEdits);
            } else {
                textEditMap.put(uri, textEdits);
            }
        }
    }
    textEditMap.forEach((uri, annotatedTextEdits) -> {
        TextDocumentEdit textDocumentEdit = new TextDocumentEdit();
        VersionedTextDocumentIdentifier identifier =
                new VersionedTextDocumentIdentifier();
        identifier.setUri(uri);
        List<TextEdit> textEdits = new ArrayList<>(annotatedTextEdits);
        textDocumentEdit.setEdits(textEdits);
        textDocumentEdit.setTextDocument(identifier);
       textDocumentEdits.add(Either.forLeft(textDocumentEdit));
    });
    return textDocumentEdits;
}
Listing 7-2

Generate Document Changes for Rename (RenameProvider.java)

enum RenameChangeAnnotation {
    withQuote("withQuote", "Quoted Rename",
            "Rename keyword with a quote"),
    withoutQuote("withoutQuote", "Un-quoted Rename",
            "Rename keyword without a quote");
    private final String id;
    private final String label;
    private final String description;
    RenameChangeAnnotation(String id, String label, String description) {
        this.id  = id;
        this.label = label;
        this.description = description;
    }
    public ChangeAnnotation get() {
        ChangeAnnotation changeAnnotation = new ChangeAnnotation();
        changeAnnotation.setDescription(this.description);
        changeAnnotation.setLabel(this.label);
        changeAnnotation.setNeedsConfirmation(true);
        return changeAnnotation;
    }
    public String getId() {
        return id;
    }
}
Listing 7-3

Define Change Annotations for Rename Operation (RenameProvider.java)

Note

At the initialization phase, we capture the workspace capabilities of the client and then pass down to the language feature APIs accordingly for decision making. This applies to the particular operation’s capabilities as well. For example, in the rename operation, before sending change annotations, check whether the client supports the particular capability.

Prepare Rename

The client sends the textDocument/prepareRename to the server to validate a renaming operation for a given location. For example, consider the example we discussed in the previous section where the user renames a keyword. As we discussed in the earlier section, both rename and prepare rename are bound together. In the initialization, under the rename capability, the client specifies whether the prepare rename operation is supported.

As the input parameters, the client sends PrepareRenameParams containing the position information. The response to the prepareRename operation can take the following shapes:
  • Range

The range of the string to be renamed. This is helpful when the user has selected a string with leading and trailing spaces, and then the server specifies the actual range of the token.
  • {range: Range, placeholder: string}

Additional placeholder for the rename input. If the server can generate meaningful names for the variables, this option can be used.
  • { defaultBehavior: boolean}

The rename token identification will be done according to the default behavior of the client.
  • Null

The particular rename operation is not valid at the given location.

In Listing 7-4, we demonstrate the prepareRename validation, where the server sends the rename range and a placeholder value as well. This placeholder value is shown in the UI, and the server can either use this placeholder value to provide a hint or a rename’s new value suggestion depending on the behavior. Listing 7-5 shows the JSON-RPC format of the textDocument/prepareRename response as per our example implementation.
public static PrepareRenameResult
    prepareRename(BalPrepareRenameContext context) {
        Token tokenAtCursor = context.getTokenAtCursor();
        if (tokenAtCursor.kind() != SyntaxKind.IDENTIFIER_TOKEN
                || CommonUtils.isKeyword(tokenAtCursor.text())) {
            return null;
        }
        LinePosition startLine = tokenAtCursor.lineRange().startLine();
        LinePosition endLine = tokenAtCursor.lineRange().endLine();
        PrepareRenameResult renameResult = new PrepareRenameResult();
        Range range = new Range();
        range.setStart(new Position(startLine.line(), startLine.offset()));
        range.setEnd(new Position(endLine.line(), endLine.offset()));
        renameResult.setPlaceholder("renamed_" + tokenAtCursor.text());
        renameResult.setRange(range);
        return renameResult;
}
Listing 7-4

Set PrepareRename (RenameProvider.java)

[Trace - 8:22:39 AM] Received response 'textDocument/prepareRename - (165)' in 5ms.
Result: {
    "range": {
        "start": {
            "line": 1,
            "character": 9
        },
        "end": {
            "line": 1,
            "character": 19
        }
    },
    "placeholder": "renamed_helloWorld"
}
Listing 7-5

Prepare Rename JSON-RPC Response

Formatting

In organizations, you have seen there are specific coding standards being specified. These include best practices such as code organization, naming conventions, and styling guidelines. All these aspects are to ensure the consistency of the code written by the developers in an organization. Among these best practices and coding standards, source code formatting has a significant importance for readability of the code as well as consistency when using version controlling systems. In LSP, the client sends the textDocument/formatting request to the server in order to format a document with the specified TextDocumentIdentifier. Other than the full document format, the Language Server Protocol allows formatting a section of the document as well, which we will be looking at in the next section.

Initialization and Capabilities

Client Capabilities

Client capabilities are specified with DocumentFormattingClientCapabilities, and the client can specify whether it allows the server to dynamically register the formatting operation by setting the dynamicRegistration property. In real-world use cases, there are tools/plugins which can be installed to format sources. In such cases, the dynamic registration of the formatting capability is important for Language Server developers. In order to handle such scenarios, we can add user configurations, and based on the configuration values, the server can dynamically register the formatting operation.

Server Capabilities

The server can specify the formatting capabilities via just setting the formatting capability to true or specify with the DocumentFormattingOptions. Similarly, the server can register the capability dynamically by setting the DocumentFormattingRegistrationOptions.

Generating the Formatting TextEdits

The textDocument/formatting request’s input parameters are represented with DocumentFormattingParams. It is important to look at the options property (FormattingOptions) which specifies the options configured and associated with the client. Usually, plugin developers can introduce custom options and read configurations via the configuration operations exposed in the Language Server. Although it is consistent and user-friendly to honor the default editor configuration options, custom configurations can be used to extend the capabilities.

The FormattingOptions exposes the following configurations:
  1. 1.

    tabSize – Size of a tab in spaces

     
  2. 2.

    insertSpaces – Whether to use tabs or spaces

     
  3. 3.

    trimTrailingWhitespace – Whether to trim trailing whitespaces of a line

     
  4. 4.

    insertFinalNewline – Whether to insert a new line at the end of the file

     
  5. 5.

    trimFinalNewlines – Whether to trim all the new lines at the end of the file

     

Other than the specified named options, the protocol allows the clients to send more properties as key-value pairs. The type of values can be string, boolean, or number.

In our example (Listing 7-6) use case, we use a formatting library implemented for Ballerina. Currently, the Ballerina formatter library honors a limited number of options.

For the formatting request, the server responds with a list of TextEdits. When generating the TextEdit for the formatting operation, we replace the full document content.
public static List<TextEdit> format(BaseOperationContext context,
                                        DocumentFormattingParams params) {
        Path path = CommonUtils.uriToPath(params.getTextDocument().getUri());
        SyntaxTree syntaxTree = context.compilerManager()
                .getSyntaxTree(path).orElseThrow();
        try {
            // Current ballerina formatter has default behaviour
            // Based on the formatter, formatting options can read and utilize
            // FormattingOptions options = params.getOptions();
            String formattedSource = Formatter.format(syntaxTree).toSourceCode();
            LinePosition eofPos = syntaxTree.rootNode().lineRange().endLine();
            // Replace the full document context
            Range range = new Range(new Position(0, 0),
                    new Position(eofPos.line() + 1, eofPos.offset()));
            TextEdit textEdit = new TextEdit(range, formattedSource);
            return Collections.singletonList(textEdit);
        } catch (FormatterException e) {
            return Collections.emptyList();
        }
}
Listing 7-6

Preparing the Formatting (FormatProvider.java)

In our example, we do not utilize the formatting options sent by the server, since the current formatter only works on the default options. In cases of using configurable formatters, the server can access the formatting options as FormattingOptions options = params.getOptions(). Not only the specified APIs but also the client can send additional properties, which can be accessed as options.get("propertyName"). It is always a better practice to use a configurable formatter since the user’s default formatting configurations can be honored; otherwise, the server as well as the client can notify the user via logs and documentations on the default behavior of the formatter to avoid conflicts.

Range Formatting

In the previous section, we had a look at the full document formatting for a source, and it is a common requirement in the development flow to format a part/range of a selected text document. This particular capability is exposed in the Language Server Protocol with textDocument/rangeFormatting.

Initialization and Capabilities

As we described in the previous section, the client capabilities and server capabilities of the rangeFormatting are similar. In our example, we set the rangeFormatting capability by setting the Boolean flag (instead of using the formatting options) statically upon the initialization request. For the server capability registration at the server initialization, you can refer to BalLanguageServer.java and ServerInitUtils.java, where there are getters for each feature registration option such as getSignatureHelpOptions and getHoverOptions. As a best practice, it is a better option to move all the registration logic to a separate utility/factory implementation to isolate the lengthy logic.

Generating the Range Formatting TextEdits

The textDocument/rangeFormatting request is sent from the client to the server with DocumentRangeFormattingParams. These input parameters include an additional property called range when compared to the DocumentFormattingParams. The options property specified in the parameters is same as we have addressed in the formatting operation earlier. The range specified in the parameters specifies the selection range where the formatting should be applied.

In our example, the Ballerina formatting library allows us to format a range of a text document. In our example implementation in Listing 7-7, we show how to invoke the formatter and to calculate the new range to be included in the TextEdit.
public static List<TextEdit> formatRange(BaseOperationContext context,
                                        DocumentRangeFormattingParams params) {
        Path path = CommonUtils.uriToPath(params.getTextDocument().getUri());
        SyntaxTree syntaxTree = context.compilerManager()
                .getSyntaxTree(path).orElseThrow();
        try {
            // Current ballerina formatter has default behaviour
            // Based on the formatter, formatting options can read and utilize
            // FormattingOptions options = params.getOptions();
            Range range = params.getRange();
            LinePosition startPos = LinePosition.from(
                    range.getStart().getLine(),
                    range.getStart().getCharacter());
            LinePosition endPos = LinePosition.from(
                    range.getEnd().getLine(),
                    range.getEnd().getCharacter());
            LineRange lineRange =LineRange.from(
                    syntaxTree.filePath(),
                    startPos, endPos);
            SyntaxTree formattedTree
                    = Formatter.format(syntaxTree, lineRange);
            LinePosition eofPos = syntaxTree.rootNode().lineRange().endLine();
            Range updateRange = new Range(new Position(0, 0),
                    new Position(eofPos.line() + 1, eofPos.offset()));
            TextEdit textEdit = new TextEdit(updateRange,
                    formattedTree.toSourceCode());
            return Collections.singletonList(textEdit);
        } catch (FormatterException e) {
            return Collections.emptyList();
        }
}
Listing 7-7

Generate the Range Formatting TextEdit (FormatProvider.java)

Note

For most of the programming languages and scripting languages, there are formatting libraries which address the most common use cases such as tab size, spaces to tab conversion, etc. For the Language Server implementation, we can use such libraries as it is, or the server implementation can use a hybrid approach by modifying the source with external libraries along with the server's specific implementation.

On Type Formatting

The textDocument/onTypeFormatting request is sent from the client to the server to request formatting while typing. The server can configure a set of trigger characters in which the onTypeFormatting should be triggered by the client. We will be looking at the example implementation at the end of this section.

Initialization and Capabilities

Client Capabilities

Client capabilities are specified with DocumentOnTypeFormattingClientCapabilities and include the dynamicRegistration property to specify whether the client supports the dynamic registration of the operation.

Server Capabilities

Server capabilities are specified with DocumentOnTypeFormattingOptions which contains two important properties where the server can specify a set of trigger characters in which the formatting should be triggered.

The firstTriggerCharacter specifies the trigger character for the formatting. Also, the server can specify more trigger characters by setting an array of characters for the moreTriggerCharacter property . In our example use case in Listing 7-8, we are setting "}" as the first trigger character and add ";" to the moreTriggerCharacter list .
public static DocumentOnTypeFormattingOptions
    getOnTypeFormatOptions() {
    DocumentOnTypeFormattingOptions options = new DocumentOnTypeFormattingOptions();
    options.setFirstTriggerCharacter("}");    options.setMoreTriggerCharacter(Collections.singletonList(";"));
    return options;
}
Listing 7-8

Set On Type Formatting Options (ServerInitUtils.java)

Depending on the language grammar and the semantics of the language, trigger characters can be varied. For example, in most of the language grammars, we have seen the usage of blocks enclosing statements and similar constructs with pairs of braces ("{" and "}"). In such scenarios, typing the closing brace ("}") can indicate completing a valid block, which the server can assume to trigger the formatting for a valid block. In the example use case (Listing 7-8), we have used a semicolon (";") as a trigger character as well. This is because Ballerina’s grammar allows the ending of a statement, top-level type definition, and expression with a semicolon, which allows the server to trigger the formatting at the end of a valid construct.

Generating the On Type Formatting TextEdits

The parameters (DocumentOnTypeFormattingParams) of the textDocument/onTypeFormatting contain two main and operation-specific properties as ch and options. The ch property specifies the character typed to trigger the formatting request, and the server can use the trigger character for validations before the formatting. The options property specifies the formatting options which are the same as we discussed in the previous sections.

As a response to the request, the server sends an array of TextEdits to the client. Listing 7-9 shows calculating the formatting TextEdits for the onTypeFormatting. In our example implementation, we have honored on type formatting for function definitions and for local variable declaration only, while the solution can be extended to support a wide range of language constructs according to the semantics. You can use the following example source snippets to test the behavior:
function addTwoNumbers      (int number1,     int number2 ) returns int {
      int sum= number1 +      number2;
      // <type } here>
function addTwoNumbers    (int number1,     int number2)      returns     int {
      int sum= number1 +      number2 // <type ; here>
}
Note

When you test the feature on VS Code, make sure you enable the On Type Formatting option on the user settings.

public static List<TextEdit>
onTypeFormat(BalPosBasedContext context,
             DocumentOnTypeFormattingParams params) {
    Path path = CommonUtils
            .uriToPath(params.getTextDocument().getUri());
    SyntaxTree syntaxTree = context.compilerManager()
            .getSyntaxTree(path).orElseThrow();
    try {
        // Current ballerina formatter has default behaviour
        // Based on the formatter, formatting options can read and utilize
        // FormattingOptions options = params.getOptions();
        LineRange lRange;
        NonTerminalNode nodeToFormat = getNodeToFormat(context.getNodeAtCursor());
        if ((params.getCh().equals("}")
                && nodeToFormat.kind() == SyntaxKind.FUNCTION_DEFINITION)
                || (params.getCh().equals(";")
                && nodeToFormat.kind() == SyntaxKind.LOCAL_VAR_DECL)) {
            lRange = nodeToFormat.lineRange();
        } else {
            return Collections.emptyList();
        }
        SyntaxTree formattedTree
                = Formatter.format(syntaxTree, lRange);
        LinePosition eofPos = syntaxTree.rootNode().lineRange().endLine();
        Range updateRange = new Range(new Position(0, 0),
                new Position(eofPos.line() + 1, eofPos.offset()));
        TextEdit textEdit = new TextEdit(updateRange,
                formattedTree.toSourceCode());
        return Collections.singletonList(textEdit);
    } catch (FormatterException e) {
        return Collections.emptyList();
    }
}
Listing 7-9

Generate On Type Formatting TextEdit

Note

All three formatting operations we discussed have slightly different behaviors which can be parameterized to work on a common, base implementation. For example, the Ballerina formatting library we used here has a tree visitor which allows formatting a text document or a part of a text document based on the syntax tree. It will take a node into consideration for formatting, and the root node as well as the subtree root is treated in a similar manner.

Code Actions

The client sends the textDocument/codeAction request to the server to compute the commands which can either be code fixes or code refactoring commands. When the server sends the list of CodeActions, the client shows the associated commands in the UI. Then the user can select the particular command, and the client will send a workspace/executeCommand request to execute the actions associated with the particular command.

When we consider a programming language, there are many alternative ways of using the language semantics for achieving the same outcome. Depending on the user’s preference, language best practices, organizational guidelines, etc., the choices can vary. Also, it has become a norm today for IDEs and editors to provide error resolving capabilities for the developers by analyzing the semantic and syntactic information of the source. For example, consider the user writes a function name, which is not defined within the scope. In such cases, editors provide quick fixes to generate the particular function within the given scope. The aforementioned use cases are a few of the most widely used refactoring and code fixing options by the developers. The LSP exposes the capability to resolve the aforementioned use cases via code actions. The server takes the control to specify the supported commands, and those can be registered at the server initialization. In our example use case in Listing 7-10, we have shown registering the commands at the startup.
public static ExecuteCommandOptions getExecCommandOptions() {
    ExecuteCommandOptions options = new ExecuteCommandOptions();
    options.setCommands(Arrays.asList(Commands.ADD_DOC,
        Commands.CREATE_VAR));
    return options;
}
Listing 7-10

Set Execute Command Options (ServerInitUtils.java)

Initialization and Capabilities

Client Capabilities

Client capabilities are specified with CodeActionClientCapabilities which contains the property dynamicRegistration to specify whether the client supports registering the operation dynamically.

The protocol allows assigning a kind (CodeActionKind) to a code action, and the client can use the kind to group code actions during the presentation in the UI. The codeActionLiteralSupport property allows the client to specify the supported set of CodeActionKind literals.

The isPreferredSupport is an optional property sent by the client to specify whether the client honors setting the isPreferred property in a code action. If the client allows setting the particular property, it considers the particular code action for the auto fix command.

The dataSupport and resolveSupport are associated properties. The client can send a codeAction/resolve request to the server in order to retrieve additional information about a certain code action. If the client supports the resolve request, then it should have the data support as well. These data are set by the server as a response to the codeAction request, and the same data will be preserved and sent with the codeAction/resolve request.

As we had a look at the annotated text edits earlier on the textDocument/rename operation, the client specifies whether it supports the annotated text edits for code actions by setting the honorsChangeAnnotations property.

Server Capabilities

Server capabilities are specified with the CodeActionOptions which contains the following properties.

The codeActionKinds property specifies a list of CodeActionKinds supported by the server. There are different CodeActionKinds, and each is represented with a dot-separated identifier. Here, we will provide a brief description about the available options and example usages:
  1. 1.

    Empty (“”)

     
  2. 2.

    QuickFix (“quickFix”)

    For quick fixes which are usually shown when hovering over a diagnostic

     
  3. 3.

    Refactor (“refactor”)

    Code actions such as creating/extracting a variable or a function can be categorized as refactoring code actions

     
  4. 4.

    Source (“source”)

    Code actions in which the effects apply to the whole document such as fix linting issues and organizing imports

     
  5. 5.

    RefactorExtract (“refactor.extract”)

    Such as extracting a variable from a function call which returns a value

     
  6. 6.

    RefactorInline (“refactor.inline”)

    Such as refactoring to an inline function

     
  7. 7.

    RefactorRewrite (“refactor.rewrite”)

    Such as adding an access modifier keyword to a function as a best practice

     
  8. 8.

    SourceOrganizeImports (“source.organizeImports”)

    Code actions which reorganize imports

     

The first four options are considered as base actions, while the remaining four options are child actions under the base actions. Setting the relevant kind can help in improving the developer experience since the clients in general organize code actions hierarchically with native user experience.

The resolveProvider property takes a boolean value to specify whether the server supports the textDocument/codeAction/resolve operation.

Generating the CodeAction

Request Parameters

The client sends the textDocument/codeAction request with CodeActionParams specifying the input data associated with the operation.

The textDocumentIdentifier property specifies the text document where the particular code action is triggered.

The range property specifies a range where the particular code action is associated with. There are two use cases where the code action is triggered for a given cursor position (both the start and the end of the range are the same), and the code action is triggered for a selection of code blocks (the start and end of the range are different).

The context (CodeActionContext) property specifies additional information associated with the code action. With the context, the server can access an array of diagnostics (Diagnostic) which overlaps with the range of the code action via the context.diagnostics property. It is not recommended to use these diagnostics to capture the actual diagnostics for the given range. Depending on the language semantics, diagnostic ranges can be different and should be captured accordingly via semantic APIs or a similar manner. In our example, we simply ignore this particular property and extract the diagnostics by analyzing the range of the text document. Depending on the requirement, the server implementation can use the diagnostics sent with the code action request while it will be a better approach to treat these diagnostics as additional data.

The only property in CodeActionContext specifies a list of CodeActionKinds. If the client sets this particular property, this means the client only considers code actions with the specified code action kind. As we specified earlier, clients can use the code action kind to group the code actions. For example, a client can show only the quick fixes in the context menu using the code action kind as the filter. The servers can honor the only property and exclude computing unnecessary code actions by saving the computation time.

Generating the Response

As a response to the textDocument/codeAction request, the server sends either an array of Commands or an array of CodeActions. Listing 7-11 is an example for sending commands as the response. Sending just the command is not scalable and informative for the client, since it does not contain additional information as in CodeAction. Once the server sends the command, the client shows the command in the UI, and upon the selection of the certain command, the client sends the workspace/executeCommand request to the server. Then the server can proceed with the workspace edit operations for the particular command.
public static List<Either<Command, CodeAction>>
getCodeAction(BalCodeActionContext context, CodeActionParams params) {
   List<Either<Command, CodeAction>> codeActions = new ArrayList<>();
   List<Diagnostic> diags = getDiagnostics(context, params.getRange());
   Optional<Node> topLevelNode =
       getTopLevelNode(context, params.getRange());
   List<String> diagMessages = diags.stream()
       .map(diag -> diag.message().toLowerCase(Locale.ROOT))
       .collect(Collectors.toList());
   if (diagMessages.contains(VAR_ASSIGNMENT_REQUIRED)) {
     Diagnostic diagnostic = diags.get(diagMessages
         .indexOf(VAR_ASSIGNMENT_REQUIRED));
     Command createVarCommand =
         getCreateVarCommand(context, diagnostic,
             params.getRange());
     Either<Command, CodeAction> command =
         Either.forLeft(createVarCommand);
     return Collections.singletonList(command);
   }
   ...
}
private static CommandgetCreateVarCommand(BalCodeActionContext context,
           Diagnostic diagnostic,
           Range range) {
   String expr = context.getNodeAtCursor().toSourceCode().trim();
   Command command = new Command();
   command.setCommand(BalCommand.CREATE_VAR.getCommand());
   command.setTitle(BalCommand.CREATE_VAR.getTitle());
   List<Object> args = new ArrayList<>();
   String typeDescriptor = getExpectedTypeDescriptor(range, context);
   String uri = context.getPath().toUri().toString();
   String newText = typeDescriptor + " varName = " + expr;
   LineRange lineRange = context.getNodeAtCursor().lineRange();
   CreateVariableArgs createVarArgs =
       new CreateVariableArgs(newText, lineRange, uri, diagnostic);
   args.add(new CommandArgument("params", createVarArgs));
   command.setArguments(args);
   return command;
}
Listing 7-11

Generate a Command as a Response (CodeActionProvider.java)

When sending CodeActions as the response, the server can set the kind of the code action. The Language Server Protocol specifies levels of code action kinds. Depending on the requirement, the server can either set a generic kind such as CodeActionKind.Refactor or a more fine-grained kind such as RefactorExtract. If both the client and the server can support code action literals, it would be better to set the code action kind meaningfully to provide a rich developer experience. The example in Listing 7-12 demonstrates the usage of CodeActionKind.QuickFix (the CodeAction variation of Listing 7-11), and Figure 7-2 shows how the client (VS Code) presents quick fixes for the developer in the user interface.
../images/509925_1_En_7_Chapter/509925_1_En_7_Fig2_HTML.jpg
Figure 7-2

VS Code’s representation of quick fixes for diagnostics

private static CodeAction
    getCreateVarCodeAction(BalCodeActionContext context,
                           Range range,
Diagnostic diagnostic,
CodeActionParams params) {
    CodeAction codeAction = new CodeAction();
    codeAction.setTitle(Command.CREATE_VAR.getTitle());
    codeAction.setKind(CodeActionKind.QuickFix);
    /*
    Setting the diagnostic will show a quickfix link when hover over the diagnostic.
    */
    codeAction.setDiagnostics(Collections
                .singletonList(getDiagnostic(diagnostic)));
    codeAction.setEdit(getWorkspaceEdit(context, range));
    ...
    return codeAction;
}
Listing 7-12

Generate a CodeAction

The diagnostics field takes a list of diagnostics that the particular code action resolves. If the particular code action is generated for a given diagnostic, then the client can use the particular diagnostics to provide various visual aids such as shown in Figure 7-2.

The isPreferred property can be set for a code action if the server expects that particular action is appropriate to be executed with the auto fix command. It is a more common behavior that there are multiple code actions for a given construct/diagnostic. In our example in Listing 7-12 where we have shown that we generate a code action for creating a variable, when we set the isPreferred property, the client will automatically apply the code action upon the auto fix command execution. When setting the field for a particular code action, the server should ensure that the particular code action addresses the resolution provided properly. Otherwise, the output of the code action might not address user expectations, and the user will have to spend cycles to revert certain unexpected changes.

The disabled property can be set by the server to disable a certain code action. Then the client should honor the following guidelines specified as in the protocol.

Disabling code actions for certain contexts completely depends on the language semantics and the implementation of the Language Server. For example, let’s consider a code action which extracts a set of statements to a function. In certain scenarios, compilers could not be able to compute the correct type information during the type checker phase if there are syntax errors which cannot be recovered in a predictable manner. One such situation is when there are multiple syntax errors. In such situations, the code action might not be able to properly capture the type information which is required to generate a new function encapsulating the statements. In these scenarios, the server can set the disabled property for the code action with an appropriate error message.

The edit property takes a WorkspaceEdit to be applied upon the selection of the code action. Calculating the workspace edit for a code action can take a while in certain scenarios. For example, consider a code action which generates an undefined function where the server has to extract the types of the arguments, the return types, as well as the location of the source. In such cases, the code action can skip setting the workspace edit and use the resolve request to compute the particular workspace edit on demand. The CreateFunctionCodeAction.java is an example that we will be discussing in the next section.

The command property allows the server to set a command associated with the code action. One important thing which should be kept in mind is, if the server sets both the edit property and the command property for the code action, the client will apply the workspace edit first, and then trigger the command. Also, when the server implementation does not support the resolve operation, still the server can avoid computing the edit for time-consuming scenarios and achieve the same via setting the command property and handling the workspace edit upon the workspace/executeCommand request.

The data property can be set by the server to preserve any data between codeAction and resolve requests. Usually, these data can be some metadata to decide the appropriate context information, without recalculating them as in the codeAction request. The context information captured in the codeAction request can be populated in the data property and reused in the resolve request. We will be looking at an example in the next section of this chapter.

Code Actions Resolve

The codeAction/resolve request is sent from the client to the server for requesting additional information for a code action. As described in the previous section, the client specifies whether it supports the code action resolve operation with the client’s code action capabilities. The client specifies the support by setting the resolveSupport with a list of properties where the client allows it to resolve with the resolve request. Listing 7-13 is a part of a trace message extracted for the code action request which specifies the supported properties to be resolved.
"codeAction": {
    "dynamicRegistration": true,
    "isPreferredSupport": true,
    "disabledSupport": true,
    "dataSupport": true,
    "resolveSupport": {
        "properties": [
            "edit"
        ]
     },
     "codeActionLiteralSupport": {
         "codeActionKind": {
            "valueSet": [
                "",
                "quickfix",
                "refactor",
                ...
            ]
         }
     },
     "honorsChangeAnnotations": false
}
Listing 7-13

CodeAction Client Capabilities

The CreateFunctionCodeAction.java demonstrates generating a code actions to create a function and we fill the data property with the location information which will be preserved for reference in the codeAction/resolve request. The input parameter of the request is a CodeAction, and the result/response is also a CodeAction with the resolved parameters filled. The CreateFunctionCodeActionResolve.java shows generating the edit for the response code action.

CodeLens

The textDocument/codeLens request is sent from the client to the server to request code lenses for the document. Code lens shows a clickable hovering link on the document which executes a command upon selection when available.

Initialization and Capabilities

Client Capabilities

Client capabilities (CodeLensClientCapabilities) of the codelens specify whether it allows dynamic registration of the operation with the dynamicRegistration property.

Server Capabilities

Server capabilities (CodeLensOptions) allow the server to specify whether it supports the code lens resolve support. This approach is slightly different from the code action and code action resolve request as we discussed in the previous section. When the server sets the resolveProvider property, then the client sends the codeLens/resolve to the server, to get the command to be executed upon the selection of the code lens.

Generating the Response

The client sends the CodeLensParams for the server which includes the document identifier for the document where the code lens request is triggered. The code lens request is not triggered for the cursor positions, instead for the full document. When the text document is modified, then the client sends a codeLens request again to recompute the code lenses.

As a response to the textDocument/codeLens request, the server sends an array of CodeLenses.

The range property in the CodeLens specifies the range where the particular code lens is associated with. It is important that the range should cover a single line.

The command property specifies the command to be executed upon the selection of the particular code lens. This is an optional property to set during the textDocument/codeLens operation. If the server needs to perform a heavy computation to define the command, then the server can get the benefit of the resolve request to calculate the command upon request. The next section looks at an example for the resolve request.

The data field is the same as the data field we discussed in the textDocument/codeAction request. The data field is preserved during the textDocument/codeLens and the codeLens/resolve request, and the server can fill metadata which is required to capture and compute the command for the code lens at the resolve request. The example in Listing 7-14 shows computing the code lens for adding documentation for a public function. In cases where the server needs to carry out heavy computation to compute the command for the CodeLens, then the server can set required metadata to the data field and depend on the resolve request to compute the relevant command.
public static List<CodeLens>
getCodeLenses(BalCodeLensContext context, CodeLensParams params) {
    List<FunctionDefinitionNode> functions = getPublicFunctions(context);
    List<CodeLens> codeLensList = new ArrayList<>();
    for (FunctionDefinitionNode function : functions) {
        CodeLens codeLens = new CodeLens();
        org.eclipse.lsp4j.Command command = new org.eclipse.lsp4j.Command();
        command.setCommand(BalCommand.ADD_DOC.getCommand());
        command.setTitle(BalCommand.ADD_DOC.getTitle());
        List<Object> args = new ArrayList<>();
        String fName = function.functionName().text();
        String uri = context.getPath().toUri().toString();
        args.add(new CommandArgument("params", new AddDocsArgs(fName, uri)));
        command.setArguments(args);
        codeLens.setCommand(command);
        // The range is set to the function name.
        // It is a must, that the range spans for a single line
        codeLens.setRange(toRange(function.functionName().lineRange()));
        codeLensList.add(codeLens);
    }
    return codeLensList;
}
Listing 7-14

CodeLens for Documenting a Public Function

CodeLens Resolve

The client sends the codeLens/resolve request to the server to resolve an associated command for the code lens. As described in the previous section, if the server supports the resolve request, then the server can avoid setting the command in the CodeLens response sent to the client, and the client can send the resolve request to request the code lens with the command.

The client sends the resolve request with CodeLens as an input, and this will preserve the data property’s content set at the codeLens response. These data filled in the data property can be used as metadata during the resolve as described in the previous section.

CodeLens Refresh

The server sends the workspace/codeLens/refresh request to the client to refresh/recalculate all the code lenses in the workspace. According to the Language Server Protocol, it is strongly recommended to use this request in special cases such as configuration changes which can affect the project and can cause recomputation of the code lenses. The client specifies whether it can support the refresh request by setting the refreshSupport property in CodeLensWorkspaceClientCapabilities. In our example implementation, we register a file watch for the Ballerina.toml configuration file, and, upon the workspace/didChangeWatchedFiles notification, the server sends the refresh request to the client. The BalWorkspaceService.java contains the didChangeWatchedFiles method where we have addressed this scenario; registering the file watch will be described in a later chapter in detail.

Summary

When it comes to composing the source codes in IDEs/text editors, the developers frequently carry out various refactorings such as formatting and symbol renaming. Also, IDEs and editors provide code fixes by analyzing the sources and based on the enforced best practices and linting rules.

The Language Server Protocol provides the renaming capability to allow renaming language constructs. Depending on the implementation, the server can define whether to rename only the symbols or extend the capability to support other workspace constructs such as keywords and so on.

In the protocol, there are three types of formatting options provided as formatting a given document, formatting a range of a given document, and formatting while typing. These capabilities can leverage the developer experience when used in an effective manner. Specially, the onTypeFormatting capability should not be used excessively with a bunch of trigger characters. Depending on the language semantics, the server can limit the trigger points leading to an effective developer experience.

Most of the time, language smartness providers suggest code refactorings, enforcing language best practices, for example, optimizing loop constructs. With the Language Server Protocol, the server can provide such capabilities with code actions. Code actions can be used not only to refactor a single document but also to refactor the entire workspace.

In this chapter, we discussed the refactoring features exposed by the Language Server Protocol. When it comes to the developer experience, navigation through the code is also very important. In the next chapter, we are going to discuss about code navigation features exposed by the Language Server Protocol.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.219.236.62