Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Client-side on-type formatting support #745

Conversation

SCWells72
Copy link
Contributor

@SCWells72 SCWells72 commented Jan 14, 2025

This adds support for configurable client-side on-type formatting that helps to keep the code in LSP-based files properly formatted as certain keys are typed, specifically close brace, statement terminators, and completion triggers.

Here's an example of the behavior without this feature for close brace:

LSP_Client_Side_On_Type_Formatting_Close_Brace_Disabled

and with this feature for close brace:

LSP_Client_Side_On_Type_Formatting_Close_Brace_Enabled

Similarly, here's an example of the behavior without this feature for statement terminator:

LSP_Client_Side_On_Type_Formatting_Statement_Terminator_Disabled

and with this feature for statement terminator:

LSP_Client_Side_On_Type_Formatting_Statement_Terminator_Enabled

Finally, here's an example of the behavior without this feature for completion trigger:

LSP_Client_Side_On_Type_Formatting_Completion_Trigger_Disabled

and with this feature for completion trigger:

LSP_Client_Side_On_Type_Formatting_Completion_Trigger_Enabled

As you can hopefully see in each case, without this feature, no contextually-aware formatting is applied as the user types, and with this feature, the code is generally kept pretty well-formatted on-the-fly.

Note that this is specifically client-side on-type formatting which is obviously different than the related LSP capability which seems to have very little support, at least in language servers I've tried (specifically TypeScript and CSS).

…configurable client-side on-type formatting for close brace, statement terminator, and completion trigger.
docs/LSPApi.md Outdated
|----------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------|
| boolean isEnabled(PsiFile file) | Returns `true` if the LSP feature is enabled for the given file and `false` otherwise. | `true` |
| boolean isSupported(PsiFile file) | Returns `true` if the LSP feature is supported for the given file and `false` otherwise. <br/>This supported state is called after starting the language server, which matches the file and user with the LSP server capabilities. | Check the server capability |
| boolean isRangeFormattingSupported(PsiFile file) | Returns `true` if the range formatting is supported for the given file and `false` otherwise. | Check the server capability |
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I noticed that isRangeFormattingSupported() was missing from the docs so I added it.

docs/LSPApi.md Outdated
| boolean isSupported(PsiFile file) | Returns `true` if the LSP feature is supported for the given file and `false` otherwise. <br/>This supported state is called after starting the language server, which matches the file and user with the LSP server capabilities. | Check the server capability |
| boolean isRangeFormattingSupported(PsiFile file) | Returns `true` if the range formatting is supported for the given file and `false` otherwise. | Check the server capability |
| boolean isExistingFormatterOverrideable(PsiFile file) | Returns `true` if existing formatters are overrideable and `false` otherwise. | `false` |
| boolean isFormatOnCloseBrace(PsiFile file) | Whether or not to format the file when close braces are typed. | `false` |
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And everything below here is for the client config options of this new feature.

/**
* Supported formatting scopes.
*/
public enum FormattingScope {
Copy link
Contributor Author

@SCWells72 SCWells72 Jan 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Part of the config of this feature is the scope to which formatting should be applied when an enabled key has been typed. Because some language servers may not report very good code blocks (CSS doesn't seem to), for example, it may be desirable to enable this feature so that the entire file is formatted properly when certain keys are typed. The default are detailed below for each aspect of the feature.

@@ -134,4 +134,123 @@ public void setServerCapabilities(@Nullable ServerCapabilities serverCapabilitie
rangeFormattingCapabilityRegistry.setServerCapabilities(serverCapabilities);
}
}

// Client configuration settings
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added the new options to LSPFormattingFeature as well.


@Nullable
@ApiStatus.Internal
public static TextRange getCodeBlockRange(@NotNull Editor editor,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Encapsulated this so that it can be used externally.

closeBraceChar,
closeBraceOffset
);
if (codeBlockRange == null) {
Copy link
Contributor Author

@SCWells72 SCWells72 Jan 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Made this more robust so that if a code block text range anchored by a brace pair character does not yield a code block, it will search for a code block as if not anchored.

}
}

List<TextRange> selectionTextRanges = LSPSelectionRangeSupport.getSelectionTextRanges(file, editor, offset);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just a result of additional encapsulation of how selection ranges are converted into text ranges.

@@ -100,6 +100,12 @@ public static Character getCodeBlockEndChar(@NotNull PsiFile file, @Nullable Cha
return codeBlockStartChar != null ? getBracePairsFwd(file).get(codeBlockStartChar) : null;
}

@NotNull
@ApiStatus.Internal
public static Map<Character, Character> getBracePairs(@NotNull PsiFile file) {
Copy link
Contributor Author

@SCWells72 SCWells72 Jan 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Made the brace pairs available externally but as an internal API.

@@ -58,7 +59,8 @@ public Result checkAutoPopup(char charTyped, @NotNull Project project, @NotNull
return result;
}

private static boolean hasLanguageServerSupportingCompletionTriggerCharacters(char charTyped, Project project, PsiFile file) {
@ApiStatus.Internal
public static boolean hasLanguageServerSupportingCompletionTriggerCharacters(char charTyped, Project project, PsiFile file) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Had to make this public so I marked it as @ApiStatus.Internal.

/**
* Typed handler for LSP4IJ-managed files that performs automatic on-type formatting for specific keystrokes.
*/
public class LSPClientSideOnTypeFormattingTypedHandler extends TypedHandlerDelegate {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the actual typedHandler implementation that does the real work.

LSPFormattingFeature formattingFeature = getClientConfigurationSettings(file);
if (formattingFeature != null) {
// Close braces
if (formattingFeature.isFormatOnCloseBrace(file)) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Close brace handling if enabled.

}

// Statement terminators
if (formattingFeature.isFormatOnStatementTerminator(file)) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Statement terminator handling if enabled.

}

// Completion triggers
if (formattingFeature.isFormatOnCompletionTrigger(file) &&
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Completion trigger handling if enabled.

Set<LanguageServerWrapper> startedLanguageServers = LanguageServiceAccessor.getInstance(project).getStartedServers();
for (LanguageServerWrapper startedLanguageServer : startedLanguageServers) {
// TODO: Is there a better way to ask if this language server supports the file?
if (startedLanguageServer.isConnectedTo(LSPIJUtils.toUri(file))) {
Copy link
Contributor Author

@SCWells72 SCWells72 Jan 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As stated above, we don't want typing to trigger a language server to start, so we look for a started language server that supports the current file. Is this the best way to ask that question?

We specifically want the started language server that supports the current file, not just whether or not any started language server supports the file.

}

// If appropriate, use the file text range
else if (formattingScope == FormattingScope.FILE) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be configured for file scope if desired, but if the language server provides enough information for reliable code blocks, it should be configured for code block scope (which is the default, of course).

if (formattingScope == FormattingScope.STATEMENT) {
List<TextRange> selectionTextRanges = LSPSelectionRangeSupport.getSelectionTextRanges(file, editor, beforeOffset);
if (!ContainerUtil.isEmpty(selectionTextRanges)) {
// Find the closest selection range that is extended to line start/end; that should be the statement
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because we don't have access to an actual AST/PSI tree here, we have to use a bit of a heuristic to find the statement. Language server support for selection ranges is almost always directly bound to the underlying AST, and therefore one of the selection ranges shouuld be for a statement in that language. And that selection range will be for full lines where extension of range start to line start and range end to line end is already performed by LSPSelectionRangeSupport.getSelectionTextRanges(). So the selection range that represents a contiguous range of full lines should in fact be the entire statement. Let me know if that doesn't make sense because it's the foundation upon which statement scope operates.

int endLineNumber = document.getLineNumber(endOffset);
int endLineEndOffset = document.getLineEndOffset(endLineNumber);
if ((endLineEndOffset == endOffset) || (endLineEndOffset == (endOffset + 1))) {
// Make sure that it ends with the terminator that was just typed
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we found a contiguous range of full lines that (ignoring trailing whitespace) end in the statement terminator character that was just typed, this is the range of the statement to be formatted.

}

// If appropriate, find the enclosing code block to format
else if (formattingScope == FormattingScope.CODE_BLOCK) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, it's valid to configure format-on-statement terminator to be for a more broad scope including the surrounding code block or file if that works better (or at all) for the associated language server. The default, of course, is statement scope.

// Just format the completion trigger
int offset = editor.getCaretModel().getOffset();
// NOTE: Right now all completion triggers are single characters, so this is safe/accurate
int beforeOffset = offset - 1;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For a completion trigger character, we just need to format that single character.

}
// Get the selection ranges and extend them to whole lines
Set<TextRange> textRanges = new LinkedHashSet<>();
List<TextRange> selectionTextRanges = LSPSelectionRangeSupport.getSelectionTextRanges(file, editor, effectiveOffset);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, this is the result of encapsulation of selection range-to-text range conversion.

@@ -117,6 +117,30 @@ public static TextRange getTextRange(@NotNull SelectionRange selectionRange,
return TextRange.create(startOffset, endOffset);
}

@NotNull
@ApiStatus.Internal
public static List<TextRange> getSelectionTextRanges(@NotNull PsiFile file,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And this is that encapsulation.

/**
* Common interface for language server definitions that support client configuration.
*/
public interface ClientConfigurableLanguageServerDefinition {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the same change that was made in #744 to facilitate client configuration-based testing.

/**
* Client-side format settings.
*/
public static class ClientConfigurationFormatSettings {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are the actual new client config settings for this feature.

@@ -27,6 +27,7 @@ public UserDefinedClientFeatures() {

// Use the extended feature implementations
setCompletionFeature(new UserDefinedCompletionFeature());
setFormattingFeature(new UserDefinedFormattingFeature());
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And we have to add the configurable formatting feature.

public class UserDefinedFormattingFeature extends LSPFormattingFeature {

@Nullable
private ClientConfigurationFormatSettings getFormatSettings() {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And we use client config to respond to these questions in the user-defined version of the feature.

* {@link com.redhat.devtools.lsp4ij.server.definition.LanguageServerDefinition} implementation to start a
* language server with a process command defined by the user.
*/
public class UserDefinedLanguageServerDefinition extends LanguageServerDefinition implements ClientConfigurableLanguageServerDefinition {
Copy link
Contributor Author

@SCWells72 SCWells72 Jan 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure why this is showing up as so different here, but the only real change is to make it client-configurable. If you hide white space-only changes in the diff, it should be more clear.

@@ -354,6 +354,10 @@ L
id="LSPFormattingAndRangeBothService"
implementation="com.redhat.devtools.lsp4ij.features.formatting.LSPFormattingAndRangeBothService"/>

<!-- Client-side on-type formatting -->
<typedHandler
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Register the typedHandler for client-side on-type formatting.

@@ -24,6 +24,67 @@
}
}
},
"format": {
Copy link
Contributor Author

@SCWells72 SCWells72 Jan 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JSON schema updates for the new client-side formatting settings and moving onTypeFormatting.enabled to format.onTypeFormatting.serverSide.enabled.

| boolean isRangeFormattingSupported(PsiFile file) | Returns `true` if the range formatting is supported for the given file and `false` otherwise. | Check the server capability |
| boolean isExistingFormatterOverrideable(PsiFile file) | Returns `true` if existing formatters are overrideable and `false` otherwise. | `false` |
| boolean isOnTypeFormattingEnabled(PsiFile file) | Whether or not server-side on-type formatting is enabled if `textDocument/onTypeFormatting` is supported by the server. | `true` |
| boolean isFormatOnCloseBrace(PsiFile file) | Whether or not to format the file when close braces are typed. | `false` |
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are the new client configuration options for client-side on-type formatting.

@@ -458,6 +458,67 @@ Here is an example with the [Java Language Server](https://github.com/eclipse-jd

![textDocument/onTypeFormatting](./images/lsp-support/textDocument_onTypeFormatting.gif)

If desired &mdash; for example, for those who want to control when formatting is performed &mdash; server-side/LSP-based
Copy link
Contributor Author

@SCWells72 SCWells72 Jan 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While updating the on-type formatting docs for this feature, I added details on how to disable server-side on-type formatting if desired.

}
```

#### Client-side On-Type Formatting
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And I documented client-side on-type formatting extensively including the TypeScript server configuration example and a demo.


![Client-side on-type formatting](./images/lsp-support/clientSideOnTypeFormatting.gif)

#### Server-side / Client-side On-Type Formatting Relationship
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also wanted to call out the relationship between server-side and client-side on-type formatting if the former is available in the language server and enabled and the latter is also enabled in config.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks so much for having taken time to write this doc, it is excellent!

… inferred from selection ranges followed by folding ranges.

Additionally LSP4IJ registers the an implementation of the `codeBlockProvider` extension point with
### Code block provider
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also updated the code block provider documentation to clearly state that code blocks are derived using selection ranges first and then folding ranges if necessary.


Below is an example with the [TypeScript Language Server](./user-defined-ls/typescript-language-server.md) showing code
block functionality. The IDE's Presentation Assistant shows the default keyboard shortcuts for each supported operating
system to trigger these actions.

![codeBlockProvider](./images/lsp-support/codeBlockProvider.gif)

### Selection range
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like it was removed but it's actually still right above the "Code block provider" section.

*/
public static class ClientConfigurationOnTypeFormattingSettings {
public static class ServerSideOnTypeFormattingSettings {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These now reflect the schema from our discussion today, specifically:

{
  "format": {
    "onTypeFormatting": {
      "serverSide": {
        "enabled": false
      },
      "clientSide": {
        "formatOnCloseBrace": true,
        "formatOnStatementTerminator": true,
        "formatOnStatementTerminatorCharacters": ";",
        "formatOnCompletionTrigger": true
      }
    }
  }
}

return settings != null ? settings.enabled : super.isOnTypeFormattingEnabled(file);
}

// Client-side on-type formatting
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are all the additions for client-side on-type formatting behavior based on client config.

@Nullable
private ClientConfigurationOnTypeFormattingSettings getOnTypeFormattingSettings() {
private ServerSideOnTypeFormattingSettings getServerSideOnTypeFormattingSettings() {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are just the changes from the client config schema restructuring.

@@ -90,7 +90,7 @@ public static void main(String[] args) {
}
""",
SIMPLE_MOCK_ON_TYPE_FORMATTING_JSON,
clientConfig -> clientConfig.onTypeFormatting.enabled = false
clientConfig -> clientConfig.format.onTypeFormatting.serverSide.enabled = false
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Had to update these for the new client config schema.

@SCWells72 SCWells72 marked this pull request as ready for review January 28, 2025 18:45
@SCWells72
Copy link
Contributor Author

@angelozerr this should now be ready for review again. It takes into account all of the discussions we've had on the topic so far, and it also updates the docs pretty extensively to explain this feature.

It's obviously a longer set of changes, so I'd recommend checking out the branch and playing with it against the TypeScript language server as you're reviewing it. You can also try it against other language servers, but see this comment about the level of support available in other language servers.

Please let me know what you need in order to get this one moving along toward a merge as it does significantly enhance the editor experience for JavaScript/TypeScript at the very least, and it's also the foundation for the last two pending PRs that I have in my queue right now.

@@ -34,12 +36,24 @@ public class LSPCodeBlockProvider implements CodeBlockProvider {
@Override
@Nullable
public TextRange getCodeBlockRange(Editor editor, PsiFile file) {
if ((editor == null) || (file == null)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add @nullable to parameters

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is one of the things about the plugin SDK that kind of drives me crazy...not all of the methods in EPs are properly annotated for parameter/return value nullability. Here's the definition of that method in the EP interface:

public interface CodeBlockProvider {
  @Nullable
  TextRange getCodeBlockRange(Editor editor, PsiFile psiFile);
}

I generally assume that anything not explicitly annotated as @NotNull is @Nullable, but in this case, looking at other implementations, it looks like it's safe to treat them as @NotNull:

    public @Nullable TextRange getCodeBlockRange(Editor editor, PsiFile psiFile) {
        int at = editor.getCaretModel().getOffset();
        ...
        PsiElement block1 = findBlock(psiFile.findElementAt(at));
    }

So in this instance, I've retained the exact signature of the interface method and added a defensive check just in case.

I can add @Nullable annotations to the parameters, but it would result in a deviation from the EP interface signature. Do you still want me to add them, or does that explain their absence?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No no dont add it if sdk doesnt define it. I though it was a miss

private static LSPFormattingFeature getClientConfigurationSettings(@NotNull PsiFile file) {
Project project = file.getProject();
// Client-side on-type formatting shouldn't trigger a language server to start
Set<LanguageServerWrapper> startedLanguageServers = LanguageServiceAccessor.getInstance(project).getStartedServers();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please remove this code and uses getLanguageServers with proper filter (ex started status) like you did in an another pr

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. The same commit also ensures that it doesn't even attempt range formatting if not supported by the language server, and I added a Go language unit test to confirm that change since that language server supports file formatting but not range formatting.

…er is identified. Now range formatting is only used if supported by the language server. Added a Go unit test to confirm that behavior since it supports file formatting but not range formatting.
if (formattingFeature.isFormatOnCloseBrace(file)) {
if (formattingFeature.isFormatOnCloseBrace(file) &&
// Make sure the formatter supports formatting of the configured scope
((formattingFeature.getFormatOnCloseBraceScope(file) == FormattingScope.FILE) || rangeFormattingSupported)) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In each case, we confirm that the configured scope is supported by the language server's formatter before even trying to format. As stated elsewhere, for language server's that don't support range formatting -- or support it pooly, e.g., the CSS language server -- the user can still enable formatting on close brace and/or statement terminator and have it format the entire file if desired.

@@ -90,6 +103,8 @@ public Result charTyped(char charTyped,

// Completion triggers
if (formattingFeature.isFormatOnCompletionTrigger(file) &&
// Make sure the formatter supports range formatting
rangeFormattingSupported &&
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Completion trigger on-type formatting is only range-based.

@NotNull PsiFile file,
@NotNull TextRange textRange) {
// If formatting the entire file, don't specify a range
if (textRange.equals(file.getTextRange())) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This ensures that the file formatter or range formatter is used as appropriate.

* range formatting, so these tests confirm its behavior when configured for file scope and also that it does nothing
* when configured for code block scope (i.e., the default scope).
*/
public class GoClientSideFormatOnCloseBraceTest extends LSPClientSideOnTypeFormattingFixtureTestCase {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added this Go language server-specific test to verify behavior for a language server that doesn't support range formatting but does support file formatting.

@@ -49,10 +49,21 @@ public abstract class LSPClientSideOnTypeFormattingFixtureTestCase extends LSPCo

private static final Pattern TYPE_INFO_PATTERN = Pattern.compile("(?ms)//\\s*type\\s*(\\S)[\\t ]*");

private boolean supportsRangeFormatting = true;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Derived test classes can specify that the tested language server doesn't support range formatting if appropriate.

@SCWells72
Copy link
Contributor Author

Created fresh branch/PR #785 to avoid issues from cumulative merging/rebasing over the past month. This should be considered the authoritative description of and discussion about these changes, but #785 is the vehicle by which they are merged.

@SCWells72 SCWells72 closed this Jan 30, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request formatting
Projects
Status: Done
Development

Successfully merging this pull request may close these issues.

2 participants