-
-
Notifications
You must be signed in to change notification settings - Fork 91
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
VSCode Extension #283
Comments
I was thinking we could do one better, and integrate it directly into omnisharp. That way, all the 'format' actions would use csharpier by default and have a smoother experience than just formatting on save. However, as per OmniSharp/omnisharp-roslyn#2167, they don't think omnisharp is the place for it. I want to try adding that functionality directly into Rosyln itself. So that way, people can specify the formatter (and version) as a nuget package, much like how Roslynator provides refactorings and code analysis. Of course, I should get off my lazy ass and start the discussion in Roslyn, but it's amazing how little time I have for anything these days 🤕 |
Interesting... if roslyn allows formatters, that would remove the need for a lot of the IDE integration work. If we do start down the VSCode extension path, I ran across this link which would be useful - https://code.visualstudio.com/blogs/2016/11/15/formatters-best-practices |
Okay, I chatted with @sharwell offline and he's of the opinion that unless a formatter handles all possible scenarios (including incomplete code that doesn't compile), it shouldn't go to Roslyn. While I don't fully agree, I respect his position. This means the roslyn path is a no-go. I'll probably create a vim extension before VS-code one (because I use the former more than the latter), but we will need some changes in csharpier cli to make it work. Keeping it running between formatsCurrently, (especially for small files) it seems the dotnet process spends more time initializing than the actual meat of parsing/formatting. In order avoid the perf hit when running on save, we should keep csharpier running as long as the editor is. We could do something complicated like make csharpier serve an http endpoint that takes the code and formats it, but I'm hoping we can get away with something simple and continue to use stdin. If we can make csharpier/dotnet not close the buffer on the Keeping version in projectOne of the reasons I wanted to push for a nuget/roslyn based approach like Roslynator is that then the formatter and version will be specified in the csproject as a nuget reference. That way, all team members who contribute to a specific project will format the same (despite has changing formatting rules between versions). This doesn't really need changes in csharpier cli I suppose, but I'm mentioning it here for completeness. The way @sharwell suggested it was to use dotnet tool manifest. Essentially, don't ship a version of csharpier with the vs-code/vim plugin, but have it invoke it as a dotnet tool. That way, if a manifest specifies the version, it will use that, otherwise it will fall back to the user's global version. |
Right now csharpier spits back out the exact code it got when it fails to compile, I'm not sure if that would qualify as handling it.
That was definitely the case when csharpier was still a prettier plugin. It started a new .net process for each file, which completely killed performance. I decided to port it to .net instead of trying to figure out how to keep the process alive. Serializing the AST to json was also taking a pretty big chunk of time and there didn't seem to be a good solution to that problem. |
I just saw #282 is done and you'd added support for stdin/stdout so I've had a go at a proof-of-concept vscode extension using that: https://github.com/saborrie/vscode-csharpier it has this nodejs function: import * as cp from "child_process";
function runFormatter(input: string): Promise<string> {
return new Promise((resolve, reject) => {
const csharpier = cp.spawn("dotnet", ["csharpier", "--write-stdout"], {
stdio: "pipe",
});
let output = "";
csharpier.stdout.on("data", (chunk) => {
output += chunk.toString();
});
csharpier.on("exit", () => {
resolve(output);
});
csharpier.stdin.write(input);
csharpier.stdin.end();
});
} Hope this helps in some way. I'll post another comment here tomorrow (or soon) with some ideas I've had. |
Awesome! I don't know anything about writing vscode extensions (yet) so this will definitely help. I'd like to focus on improving the formatting of csharpier, but once it gets to something we can call beta then take on things like the VSCode extension. But if you or anyone else wants to work on the extension sooner just let me know if there is anything I can do to help |
I've been busy but I've tried to throw together some notes and a diagram: Would it be best to move any vscode extension (plus any other extra packages) into the main Csharpier repo? Vscode is nodejs based, in my proof-of-concept extension I wrote a function in node which can call csharpier by spawning another process. I think it would make sense to set this up as a standalone As shocklateboy92 wrote, it would be good to have a way to fix the Csharpier version used within a project - especially if the formatting of csharpier continues to be worked on. This means that there will need to be a way of installing a local version of csharpier on a per project basis. However you do this (perhaps with a A possible structure: |
Thanks for your work (and diagrams) @saborrie! Much appreciated ❤️
Yes. Create a
We will most likely do so in the future, but I don't think we need to just yet.
How did you find the performance in your POC? Did it make saving things in VS Code feel "sluggish"?
Any particular reason? Do you envision many other projects needing to invoke csharpier in Js/Ts?
So, dotnet tools already support a per-workspace manifest/versioning like npm/yarn's You should add some helpful messaging in the UI explaining this to users and maybe some functionality to install a local/global version easily. Not sure what this would look like. |
It was slightly sluggish - I even tried spawning the process before the formatting function call and then only piping stdin but it made no difference. I think it does need to be done with a single long running process rather than stopping and starting csharpier, but I'm not sure how. I might have suggested gRPC if not for seeing this: https://gist.github.com/badsyntax/9827722afcb33a4b0e03c809f1aede98. Perhaps it's possible to use protobuf over stdin/stdout which would have the advantage over just sending the formatted text as it can support metadata like time taken, errors etc.
I'll give that a try in the POC and see what it takes to detect whether it's installed in the local folder. I did think of a problem which is that the vscode editor might not actually be open in the same directory as any manifest files, and when particular files get formatted, they might be within a subdirectory that had a different manifest file than the root, plus vscode supports formatting unsaved/untitled files. |
As someone who knows almost nothing about writing console apps or stdin/stdout beyond what I learned working on csharpier and knows nothing about protobuf or gRPC.... I played around with trying to get a POC of a long running console app that could read multiple files from stdin. The potential solution I came up with was sending a specific set of characters to the app to indicate the end of a file. When the app sees those characters it formats what it already read, and then waits for more data on stdin. It feels hacky, would need to key off of something that is guaranteed to not exist in a real file, couldn't really read line by line because csharpier needs to see the line endings, and I have no idea what reading character by character may do to performance. There really should be a better way to do this, but trying to google it didn't get me anywhere. The POC const longRunner = spawn("LongRunner.exe", [], {
stdio: "pipe"
});
longRunner.stdout.on("data", chunk => {
console.log(chunk.toString());
});
longRunner.stdin.write("test1\n")
longRunner.stdin.write("blah blah\n")
longRunner.stdin.write("ENDOFFILE\n")
await sleep(100);
longRunner.stdin.write("test2\n")
longRunner.stdin.write("ENDOFFILE\n")
await sleep(100); using var streamReader = new StreamReader(
Console.OpenStandardInput(),
Console.InputEncoding
);
var stringBuilder = new StringBuilder();
var line = await streamReader.ReadLineAsync();
while (line != null)
{
if (line == "ENDOFFILE")
{
Console.WriteLine("file: ");
Console.WriteLine(stringBuilder.ToString());
stringBuilder = new StringBuilder();
}
else
{
stringBuilder.AppendLine(line);
}
line = await streamReader.ReadLineAsync();
} |
That's more or less what I was thinking, except using the EOF byte to signal the end of the file, instead of a string that can appear in a document like As I type this, it occurred to me that we could also base64 (or Json) encode the whole file, so we know where the end is. Less efficient, but safer since it can't be broken by any user input. |
I knew there was something like this that existed, but I wasn't failing when googling EOF character and NULL character. I did some digging into the performance of CSharpier against a single file. By far the slowest part is roslyn initializing behind the scenes. It is ~40% of the time. That cost is only paid for the first file that is formatted. Even if we could optimize those last three, that doesn't seem like enough of a gain to make the sluggish feel go away. Keeping csharpier running to format files seems like the way to go. |
Hi, I made an extension for vscode just to test it out for my personal use and it seems to work OK for me so I thought I could share it here if it could be of interest to you. https://github.com/pontusntengnas/CSharpier-vscode
Its not super fast no, it works for me but could be improved, I see that you have been looking at a long running process and that seems like a good idea. |
Out of curiosity, I've had a go at length-delimited-protobuf over stdio between NodeJS and C#: https://github.com/saborrie/node-csharp-protobuf-stdin-poc I don't know whether this is an ultimately better or worse choice than using the EOF byte - it is possible length delimited is a bad idea, but it does add some interesting capabilities:
the C# server looks like this: using System;
using System.IO;
using Formatter.Protos;
using Google.Protobuf;
namespace server
{
class Program
{
static void Main(string[] args)
{
using var stdin = Console.OpenStandardInput();
using var stdout = Console.OpenStandardOutput();
FormatRequest message;
while ((message = FormatRequest.Parser.ParseDelimitedFrom(stdin)) != null)
{
var response = new FormatResponse
{
Content = message.Content.ToUpper() + "!",
Ms = 10
};
CodedOutputStream codedOutput = new CodedOutputStream(stdout);
codedOutput.WriteFixed32((uint)response.CalculateSize());
response.WriteTo(codedOutput);
codedOutput.Flush();
}
}
}
} and the NodeJS client looks like this: const proto = require("protobufjs");
const path = require("path");
const cp = require("child_process");
const server = cp.exec("dotnet run", {
stdio: "pipe",
cwd: path.resolve(__dirname, "../server"),
});
const callbacks = [];
// hack to convice the server.stdout to return binary buffers
if (server.stdout._readableState) {
delete server.stdout._readableState.decoder;
delete server.stdout._readableState.encoding;
}
async function main() {
const root = await proto.load(path.resolve(__dirname, "../Formatter.proto"));
const FormatRequest = root.lookupType("FormatRequest");
const FormatResponse = root.lookupType("FormatResponse");
// every time the stream becomes readable, emit all the events from it.
server.stdout.on("readable", function () {
let sizeb;
while (null !== (sizeb = server.stdout.read(4))) {
const size = sizeb.readInt32LE();
const bytes = server.stdout.read(size);
var message = FormatResponse.decode(bytes);
const callback = callbacks.shift();
if (callback) {
callback(message);
}
}
});
function sendMessage(request) {
const requestBuffer = FormatRequest.encodeDelimited(request).finish();
var response = new Promise((resolve) => {
callbacks.push(resolve);
});
server.stdin.write(requestBuffer);
return response;
}
console.log(await sendMessage({ content: "message 1", setting1: true }));
console.log(await sendMessage({ content: "message 2", setting1: true }));
console.log(await sendMessage({ content: "message 3", setting1: true }));
console.log(await sendMessage({ content: "message 4", setting1: true }));
console.log(await sendMessage({ content: "message 5", setting1: true }));
console.log(await sendMessage({ content: "message 6", setting1: true }));
console.log(await sendMessage({ content: "message 7", setting1: true }));
console.log(await sendMessage({ content: "message 8", setting1: true }));
}
main(); |
Here are some of my thoughts after I finally spent some time digging into this more. Modifying csharpier to support a long running mode - we'd want it to work for a VSCode extension but also potentially for VS/Rider extensions and bash scripts. I built out a POC that sends a file name + file content to a long running .net process, delimiting the values with \u0003, which is the End of Text control character. It seemed more appropriate than the End of File control. The server portion of that code is more involved than the protobuf POC, but the client code is simpler. I don't believe we will need to send anything to csharpier besides file name and optionally file content. And I don't think csharpier needs to respond with anything except for the file content. I am inclined to go with delimiting values for now. If we need to send more configuration options, or we need to send back error messages then we could switch to protobuf. Another possibility that I haven't really put any time into yet, would be calling c# directly from JS with something like https://github.com/tjanczuk/edge, but that would probably only apply to a VSCode extension. |
I use this setup in VSCode:
Then add this task: Workspace tasks.json: {
"version": "2.0.0",
"tasks": [
// Other tasks omitted
{
"label": "format (current file)",
"type": "shell",
"command": "dotnet csharpier ${relativeFile}",
},
]
} Workspace settings.json: {
"triggerTaskOnSave.restart": false,
"triggerTaskOnSave.on": true,
"triggerTaskOnSave.tasks": {
"format (current file)": ["**/*.cs"]
}
} It kind of works. It's notable that official prettier plugin formats the code before saving, but csharpier does the saving twice: once with bad formatting, and then again when csharpier kicks in causing editor to "blink" on save. This is probably required tho because chsarpier is pretty slow, but I don't care, it works! |
* Implementing a VScode extension + a plugin mode for csharpier closes #283 * Self code revier * Fixing issues with vstests
The initial version of this is out https://marketplace.visualstudio.com/items?itemName=csharpier.csharpier-vscode |
Great! I assume it doesn't yet leave it running in the background as it gives me ~600ms time for formatting single file:
|
Hmm, I updated the global tool to v0.12, it now works faster, but too fast for it's own sake, it cuts my file in half! It seems to sometimes remove characters from the end of the file, causing it to corrupt. I opened an issue #500 |
It'll only leave it running if csharpier is >= 0.12.0. It was pretty easy to make the extension work with < 0.12.0 as well by just piping a single file. |
It would be great to have an extension similar to https://github.com/prettier/prettier-vscode
This is mainly just a placeholder for getting the work started and determining what needs to be done.
Format on save will require #282
The text was updated successfully, but these errors were encountered: