Skip to content

Commit

Permalink
Working on updates to rider plugin to support csharpier 1.0.0 (#1433)
Browse files Browse the repository at this point in the history
  • Loading branch information
belav authored Jan 20, 2025
1 parent 2462f20 commit fe57b44
Show file tree
Hide file tree
Showing 20 changed files with 215 additions and 103 deletions.
8 changes: 8 additions & 0 deletions Src/CSharpier.Cli/Server/CSharpierServiceImplementation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@ CancellationToken cancellationToken
try
{
logger.LogInformation("Received request to format " + formatFileParameter.fileName);
if (formatFileParameter.fileContents.StartsWith("// csh-slow"))
{
Thread.Sleep(TimeSpan.FromSeconds(5));
}
if (formatFileParameter.fileContents.StartsWith("// csh-throw"))
{
throw new Exception("Throwing because of // csh-throw comment");
}
var directoryName = this.fileSystem.Path.GetDirectoryName(formatFileParameter.fileName);
DebugLogger.Log(directoryName ?? string.Empty);
if (directoryName == null)
Expand Down
6 changes: 6 additions & 0 deletions Src/CSharpier.Rider/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

# csharpier-rider Changelog

## [2.0.0]
- Adding support for CSharpier 1.0.0 which includes the ability to format xml files.
- Improving logic around starting up CSharpier to minimize UI lag.
- Wait at most two seconds to format a file on save.
- Wait longer than two seconds to start CSharpier Server, on some systems it is talking 8+ seconds.

## [1.8.4]
- Use working directory to install csharpier so it respects a projects NuGet.config

Expand Down
11 changes: 8 additions & 3 deletions Src/CSharpier.Rider/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,19 @@

<!-- Plugin description -->

This plugin makes use of the dotnet tool [CSharpier](https://github.com/belav/csharpier) to format your code. CSharpier an opinionated code formatter for c#.
It uses Roslyn to parse your code and re-prints it using its own rules.
The printing process was ported from [prettier](https://prettier.io/) but has evolved over time.
This plugin makes use of the dotnet tool [CSharpier](https://github.com/belav/csharpier) to format your code and is versioned independently.

CSharpier is an opinionated code formatter for c# and xml. \
It provides very few options and provides a deterministic way to enforce formatting of your code. \
The printing process was ported from [prettier](https://prettier.io) but has evolved over time. \

## CSharpier Version

The plugin determines which version of csharpier is needed to format a give file by looking for a dotnet manifest file. If one is not found it looks for a globally installed version of CSharpier.

## XML Formatting
Formatting XML is only available using CSharpier >= 1.0.0

### To format files:

- Install csharpier
Expand Down
2 changes: 1 addition & 1 deletion Src/CSharpier.Rider/gradle.properties
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
pluginVersion = 1.8.4
pluginVersion = 2.0.0

# See https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html
pluginSinceBuild = 222
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.project.Project;
import java.io.*;
import java.lang.Runtime.Version;
import java.nio.charset.Charset;

public class CSharpierProcessPipeMultipleFiles implements ICSharpierProcess, Disposable {
Expand Down Expand Up @@ -38,8 +39,12 @@ public CSharpierProcessPipeMultipleFiles(
}

private void startProcess() {
var newCommandsVersion = "1.0.0-alpha1";
var argument = Semver.gte(this.version, newCommandsVersion)
? "pipe-files"
: "--pipe-multiple-files";
try {
var processBuilder = new ProcessBuilder(this.csharpierPath, "--pipe-multiple-files");
var processBuilder = new ProcessBuilder(this.csharpierPath, argument);
processBuilder.environment().put("DOTNET_NOLOGO", "1");
processBuilder.environment().put("DOTNET_ROOT", this.dotNetProvider.getDotNetRoot());
this.process = processBuilder.start();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
package com.intellij.csharpier;

import com.google.gson.Gson;
import com.google.gson.JsonObject;
import com.intellij.notification.NotificationGroupManager;
import com.intellij.notification.NotificationType;
import com.intellij.openapi.Disposable;
Expand All @@ -10,16 +8,13 @@
import com.intellij.openapi.editor.event.DocumentEvent;
import com.intellij.openapi.editor.event.DocumentListener;
import com.intellij.openapi.fileEditor.FileDocumentManager;
import com.intellij.openapi.fileEditor.FileEditorManager;
import com.intellij.openapi.project.Project;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Instant;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import javax.xml.parsers.DocumentBuilderFactory;
Expand Down Expand Up @@ -64,6 +59,7 @@ public void documentChanged(@NotNull DocumentEvent event) {
return;
}
var filePath = file.getPath();

this.findAndWarmProcess(filePath);
}

Expand Down Expand Up @@ -93,10 +89,16 @@ private void findAndWarmProcess(String filePath) {
}

if (!this.csharpierProcessesByVersion.containsKey(version)) {
this.csharpierProcessesByVersion.put(
version,
this.setupCSharpierProcess(directory, version)
);
var finalVersion = version;
Runnable task = () -> {
this.csharpierProcessesByVersion.put(
finalVersion,
this.setupCSharpierProcess(directory, finalVersion)
);
};

var thread = new Thread(task);
thread.start();
}
}

Expand Down Expand Up @@ -256,23 +258,19 @@ private ICSharpierProcess setupCSharpierProcess(String directory, String version

this.logger.debug("Adding new version " + version + " process for " + directory);

// ComparableVersion was unhappy in rider 2023, this code should probably just go away
// but there are still 0.12 and 0.14 downloads happening
var installedVersion = version.split("\\.");
var versionWeCareAbout = Integer.parseInt(installedVersion[1]);
var serverVersion = 29;
var serverVersion = "0.29.0";

ICSharpierProcess csharpierProcess;
if (
versionWeCareAbout >= serverVersion &&
Semver.gte(version, serverVersion) &&
!CSharpierSettings.getInstance(this.project).getDisableCSharpierServer()
) {
csharpierProcess = new CSharpierProcessServer(customPath, version, this.project);
} else if (versionWeCareAbout >= 12) {
var useUtf8 = versionWeCareAbout >= 14;
} else if (Semver.gte(version, "0.12.0")) {
var useUtf8 = Semver.gte(version, "0.14.0");

if (
versionWeCareAbout >= serverVersion &&
Semver.gte(version, serverVersion) &&
CSharpierSettings.getInstance(this.project).getDisableCSharpierServer()
) {
this.logger.debug(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,56 +28,66 @@ public CSharpierProcessServer(String csharpierPath, String version, Project proj
this.dotNetProvider = DotNetProvider.getInstance(project);
this.version = version;

if (!this.startProcess()) {
this.processFailedToStart = true;
return;
}
this.startProcess();

this.logger.debug("Warm CSharpier with initial format");
// warm by formatting a file twice, the 3rd time is when it gets really fast
this.formatFile("public class ClassName { }", "/Temp/Test.cs");
this.formatFile("public class ClassName { }", "/Temp/Test.cs");
}

private boolean startProcess() {
private void startProcess() {
var newCommandsVersion = "1.0.0-alpha1";
var argument = Semver.gte(this.version, newCommandsVersion) ? "server" : "--server";
try {
var processBuilder = new ProcessBuilder(this.csharpierPath, "--server");
processBuilder.redirectErrorStream(true);
var processBuilder = new ProcessBuilder(this.csharpierPath, argument);
processBuilder.environment().put("DOTNET_NOLOGO", "1");
processBuilder.environment().put("DOTNET_ROOT", this.dotNetProvider.getDotNetRoot());
this.process = processBuilder.start();

var reader = new BufferedReader(new InputStreamReader(this.process.getInputStream()));

var executor = Executors.newSingleThreadExecutor();
var future = executor.submit(() -> reader.readLine());

String output;
try {
output = future.get(2, TimeUnit.SECONDS);
} catch (TimeoutException e) {
this.logger.warn(
"Spawning the csharpier server timed out. Formatting cannot occur."
);
this.process.destroy();
return false;
}

if (!this.process.isAlive()) {
this.logger.warn(
"Spawning the csharpier server failed because it exited. " + output
);
return false;
}

var portString = output.replace("Started on ", "");
this.port = Integer.parseInt(portString);

this.logger.debug("Connecting via port " + portString);
return true;
var csharpierProcess = processBuilder.start();

var stdoutThread = new Thread(() -> {
try (
var reader = new BufferedReader(
new InputStreamReader(csharpierProcess.getInputStream())
)
) {
var line = reader.readLine();
if (line == null) {
return;
}

var portString = line.replace("Started on ", "");
this.port = Integer.parseInt(portString);

this.logger.debug("Connecting via port " + portString);
this.process = csharpierProcess;
} catch (Exception e) {
e.printStackTrace();
}
});
stdoutThread.start();
stdoutThread.join();

csharpierProcess
.onExit()
.thenAccept(p -> {
try (
var errorReader = new BufferedReader(
new InputStreamReader(csharpierProcess.getErrorStream())
)
) {
var error = new StringBuilder();
errorReader.lines().forEach(o -> error.append(o + "\n"));
this.logger.error("Process failed to start with " + error);
} catch (Exception e) {
this.logger.error("Process failed to start with " + e);
}

this.processFailedToStart = true;
});
} catch (Exception e) {
this.logger.warn("Failed to spawn the needed csharpier server.", e);
return false;
this.processFailedToStart = true;
}
}

Expand All @@ -88,6 +98,20 @@ public FormatFileResult formatFile(FormatFileParameter parameter) {
return null;
}

var timeWaited = 0;
while (this.process == null && timeWaited < 15000) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {}

timeWaited += 100;
}

if (this.processFailedToStart || this.process == null) {
this.logger.warn("CSharpier process failed to start. Formatting cannot occur.");
return null;
}

var url = "http://127.0.0.1:" + this.port + "/format";

try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,16 @@ public void setRunOnSave(boolean value) {
this.runOnSave = value;
}

private boolean useCustomPath;

public boolean getUseCustomPath() {
return this.useCustomPath;
}

public void setUseCustomPath(boolean value) {
this.useCustomPath = value;
}

private String customPath;

public String getCustomPath() {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.intellij.csharpier;

import com.intellij.openapi.options.ConfigurationException;
import com.intellij.openapi.options.SearchableConfigurable;
import com.intellij.openapi.project.Project;
import com.intellij.ui.components.JBCheckBox;
Expand All @@ -9,7 +8,6 @@
import com.intellij.util.ui.FormBuilder;
import java.awt.*;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import org.jetbrains.annotations.Nls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
Expand All @@ -19,6 +17,7 @@ public class CSharpierSettingsComponent implements SearchableConfigurable {
private final Project project;
private JBCheckBox runOnSaveCheckBox = new JBCheckBox("Run on Save");
private JBCheckBox disableCSharpierServerCheckBox = new JBCheckBox("Disable CSharpier Server");
private JBCheckBox useCustomPath = new JBCheckBox("Override CSharpier Executable");
private JBTextField customPathTextField = new JBTextField();

public CSharpierSettingsComponent(@NotNull Project project) {
Expand Down Expand Up @@ -71,8 +70,9 @@ private JComponent createSectionHeader(String label) {
.setFormLeftIndent(0)
.addComponent(createSectionHeader("Developer Settings"), 20)
.setFormLeftIndent(leftIndent)
.addComponent(this.useCustomPath, topInset)
.addLabeledComponent(
new JBLabel("Directory of custom dotnet-csharpier:"),
new JBLabel("Directory of custom CSharpier executable:"),
this.customPathTextField,
topInset,
false
Expand All @@ -89,6 +89,8 @@ public boolean isModified() {
this.runOnSaveCheckBox.isSelected() ||
CSharpierSettings.getInstance(this.project).getCustomPath() !=
this.customPathTextField.getText() ||
CSharpierSettings.getInstance(this.project).getUseCustomPath() !=
this.useCustomPath.isSelected() ||
CSharpierSettings.getInstance(this.project).getDisableCSharpierServer() !=
this.disableCSharpierServerCheckBox.isSelected()
);
Expand All @@ -101,12 +103,14 @@ public void apply() {
settings.setRunOnSave(this.runOnSaveCheckBox.isSelected());
settings.setCustomPath(this.customPathTextField.getText());
settings.setDisableCSharpierServer(this.disableCSharpierServerCheckBox.isSelected());
settings.setUseCustomPath(this.useCustomPath.isSelected());
}

@Override
public void reset() {
var settings = CSharpierSettings.getInstance(this.project);
this.runOnSaveCheckBox.setSelected(settings.getRunOnSave());
this.useCustomPath.setSelected(settings.getUseCustomPath());
this.customPathTextField.setText(settings.getCustomPath());
this.disableCSharpierServerCheckBox.setSelected(settings.getDisableCSharpierServer());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ class CSharpierStartup : StartupActivity, DumbAware {
private val lifetimeDefinition = LifetimeDefinition()
private val lifetime: Lifetime = lifetimeDefinition


override fun runActivity(project: Project) {
// this ensures we meet the conditions of isProjectModelReady "Must be executed on UI thread or background threads with special permissions"
application.invokeLater {
Expand Down
Loading

0 comments on commit fe57b44

Please sign in to comment.