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.
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.
Generate Document Changes for Rename (RenameProvider.java)
Define Change Annotations for Rename Operation (RenameProvider.java)
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.
Range
{range: Range, placeholder: string}
{ defaultBehavior: boolean}
Null
The particular rename operation is not valid at the given location.
Set PrepareRename (RenameProvider.java)
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.
- 1.
tabSize – Size of a tab in spaces
- 2.
insertSpaces – Whether to use tabs or spaces
- 3.
trimTrailingWhitespace – Whether to trim trailing whitespaces of a line
- 4.
insertFinalNewline – Whether to insert a new line at the end of the file
- 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.
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.
Generate the Range Formatting TextEdit (FormatProvider.java)
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.
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.
When you test the feature on VS Code, make sure you enable the On Type Formatting option on the user settings.
Generate On Type Formatting TextEdit
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.
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.
- 1.
Empty (“”)
- 2.
QuickFix (“quickFix”)
For quick fixes which are usually shown when hovering over a diagnostic
- 3.
Refactor (“refactor”)
Code actions such as creating/extracting a variable or a function can be categorized as refactoring code actions
- 4.
Source (“source”)
Code actions in which the effects apply to the whole document such as fix linting issues and organizing imports
- 5.
RefactorExtract (“refactor.extract”)
Such as extracting a variable from a function call which returns a value
- 6.
RefactorInline (“refactor.inline”)
Such as refactoring to an inline function
- 7.
RefactorRewrite (“refactor.rewrite”)
Such as adding an access modifier keyword to a function as a best practice
- 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
Generate a Command as a Response (CodeActionProvider.java)
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
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.
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.