├── .github
└── dependabot.yml
├── .gitignore
├── Readme.md
├── Release Notes.md
├── Usage.md
├── build.ps1
└── src
├── .gitignore
├── FantomasVs.Shared
├── ContentTypeNames.cs
├── CustomLineChunker.cs
├── FantomasHandler.cs
├── FantomasOptionsPage.cs
├── FantomasVs.Shared.projitems
├── FantomasVs.Shared.shproj
├── FantomasVsPackage.cs
├── InstallChoice.xaml
├── InstallChoice.xaml.cs
├── InstallResultDialog.xaml
├── InstallResultDialog.xaml.cs
├── OuptutLogging.cs
├── PredefinedCommandHandlerNames.cs
└── Theme.cs
├── FantomasVs.VS2019
├── FantomasVs.VS2019.csproj
├── Properties
│ └── AssemblyInfo.cs
├── source.extension.vsixmanifest
└── template.vsixmanifest
├── FantomasVs.VS2022
├── FantomasVs.VS2022.csproj
├── Properties
│ └── AssemblyInfo.cs
├── source.extension.vsixmanifest
└── template.vsixmanifest
├── FantomasVs.sln
├── ReleaseNotes.html
└── Resources
├── License.txt
├── ReleaseNotes.html
└── logo.png
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: nuget
4 | directory: "/src/FantomasVs.VS2019"
5 | schedule:
6 | interval: daily
7 | time: "10:00"
8 | open-pull-requests-limit: 10
9 | allow:
10 | - dependency-name: "Fantomas.Client"
11 | - package-ecosystem: nuget
12 | directory: "/src/FantomasVs.VS2022"
13 | schedule:
14 | interval: daily
15 | time: "10:00"
16 | open-pull-requests-limit: 10
17 | allow:
18 | - dependency-name: "Fantomas.Client"
19 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | #Ignore thumbnails created by Windows
3 | Thumbs.db
4 | #Ignore files built by Visual Studio
5 | *.obj
6 | *.exe
7 | *.pdb
8 | *.user
9 | *.aps
10 | *.pch
11 | *.vspscc
12 | *_i.c
13 | *_p.c
14 | *.ncb
15 | *.suo
16 | *.tlb
17 | *.tlh
18 | *.bak
19 | *.cache
20 | *.ilk
21 | *.log
22 | [Bb]in
23 | [Dd]ebug*/
24 | *.lib
25 | *.sbr
26 | obj/
27 | [Rr]elease*/
28 | _ReSharper*/
29 | [Tt]est[Rr]esult*
30 | .vs/
31 | #Nuget packages folder
32 | packages/
33 | *.vsix
34 |
35 | #Rider
36 | .idea/
--------------------------------------------------------------------------------
/Readme.md:
--------------------------------------------------------------------------------
1 | # Formatting For F#
2 |
3 | This is a front-end for the wonderful [**Fantomas**](https://github.com/fsprojects/fantomas/) project.
4 |
5 | ## Features
6 |
7 | - Format Document
8 | - Format Selection
9 | - Format On Save
10 | - Patching in formatting changes
11 | - Fast, in-process formatting
12 | - Fully asynchronous, does not increase project load times
13 | - Loaded when required
14 | - Respects key-bindings
15 |
16 | ## Options
17 |
18 | Formatting options can be found in `F# Tools > Formatting`
19 |
20 | 
21 |
22 | ## In action
23 |
24 | 
25 |
26 | ## See an issue?
27 |
28 | If you see an issue with the Visual Studio integration or with configuration, please [file it here](https://github.com/deviousasti/fsharp-formatting-for-vs/issues).
29 |
30 | ## License
31 |
32 | This project is licensed under the MIT license, a copy of which can be found [here](src/License.txt).
--------------------------------------------------------------------------------
/Release Notes.md:
--------------------------------------------------------------------------------
1 | Release Notes
2 | ============
3 |
4 | 1.3.0
5 | -----
6 | * Add initial support for Fantomas cursor
7 | * Upgrade `Fantomas.Client` to `0.7.0` (#51)
8 |
9 | 1.2.0
10 | -----
11 | * Fix dotnet tool install name and fantomas docs link (#41) thanks to @AkosLukacs
12 | * Fix broken documentation links (#42)
13 | * Upgrade `Fantomas.Client` to `0.7.0` (#42)
14 | * Upgrade `editorconfig` to `0.13` (#42)
15 |
16 | 1.1.0
17 | -----
18 | * Improved logging and better diagnostics
19 | * Update `Fantomas.Client` to `0.5.4`
20 |
21 | 1.0
22 | ----
23 |
24 | * Use `Fantomas.Client` which supports installing and using Fantomas as a dotnet tools instead of being bundled with the extension
25 |
26 | 0.9.4
27 | ----
28 |
29 | * Update Fantomas to 4.5.8
30 |
31 | 0.9.3
32 | ----
33 |
34 | * Update Fantomas to 4.5.5
35 |
36 | 0.9.2
37 | ----
38 |
39 | * Update Fantomas to 4.5.4
40 |
41 | 0.9.1
42 | ----
43 |
44 | * Update stable Fantomas to 4.5.1
45 | * Update latest Fantomas to 4.5.1
46 | * Add 'Always place bar in front of discriminated union' option
47 |
48 | 0.9
49 | ----
50 |
51 | * Update to Fantomas to 4.5 alpha 17
52 | * Add guard to diff algorithm
53 |
54 | 0.8.4
55 | ----
56 |
57 | * Implement diff shrinking for minimal edit surface
58 |
59 | 0.8.3
60 | ----
61 |
62 | * Unix line-endings supported with diff
63 | * Implement line-ending agnostic chunker
64 |
65 | 0.8.2
66 | ----
67 |
68 | * Update stable Fantomas to 4.4.0
69 | * Update latest to 4.5.0 alpha
70 |
71 | 0.8.1
72 | ----
73 |
74 | * Formatting defaults to stable version
75 | * Update Fantomas to 4.4.0-beta-008
76 |
77 | 0.8
78 | ----
79 |
80 | * Change project build structure
81 | * Support both latest (bleeding edge) and stable versions configurable by an option
82 | * Update Fantomas to 4.4.0-beta-003
83 |
84 | 0.7.3
85 | ----
86 |
87 | * Add options to allow to not commit changes caused by format-on-save
88 |
89 | 0.7.2
90 | ----
91 |
92 | * Format on Save forces save if formatting results in a change
93 | * Update Fantomas to 4.4.0 alpha
94 | * Update to FCS 38.0.2
95 |
96 | 0.7.1
97 | ----
98 |
99 | * Fantomas Bugfix release
100 | * Fantomas 4.3
101 |
102 | 0.7
103 | ----
104 |
105 | * Built against Fantomas 4.3 alpha
106 | * Add support for line-ending options (and more)
107 |
108 | 0.6
109 | ----
110 |
111 | * Update to FCS 38
112 | * Built against Fantomas 4.2
113 |
114 | 0.5
115 | ----
116 |
117 | * Support MultilineFormatter selection and additional options
118 | * Built against Fantomas late September release
119 |
120 | 0.4.6
121 | ----
122 |
123 | * Built against Fantomas 4.1.1 stable
124 |
125 | 0.4.5
126 | ----
127 |
128 | * Built against Fantomas 4.1 September release
129 |
130 | 0.4.4
131 | ----
132 |
133 | * Fantomas 4.0.0 stable August release
134 | * Format selection in Fantomas instead of isolated selection (still sensitive to whitespace)
135 |
136 | 0.4.3
137 | ----
138 |
139 | * Moved to FCS 37.0
140 | * Built against 4.0.0 beta-004
141 |
142 | 0.4.2
143 | ----
144 |
145 | * Built against 4.0.0 beta-002
146 | * Directly built for net48, no ILRepack
147 |
148 | 0.4.1
149 | ----
150 |
151 | * Built against 4.0.0 beta-001
152 | * Upgrade vs build tools
153 |
154 | 0.4
155 | ----
156 |
157 | * Add MIT license
158 | * Built against 4.0.0 alpha-013
159 |
160 | 0.3
161 | ----
162 |
163 | * Add support for .editorconfig
164 | * Falls back to Visual Stuio configuration if no .editorconfig settings are found
165 | * Built against 4.0.0 alpha (17bfa28ad)
166 |
167 | 0.2
168 | ----
169 |
170 | * Built against 4.0.0 alpha (28ed449c)
171 | * Update format settings in VS config
172 |
173 | 0.1
174 | ----
175 |
176 | * Initial Release
177 | * Built against 3.0.0
178 | * Document Format
179 | * Selection Format
180 | * Format on Save
181 | * Diff format
182 |
183 |
--------------------------------------------------------------------------------
/Usage.md:
--------------------------------------------------------------------------------
1 | # Formatting For F#
2 |
3 | This extension serves as the default code formatter for F# source files (`.fs`, `.fsi`, `.fsx`).
4 |
5 | Formatting is generated by the wonderful [**Fantomas**](https://github.com/fsprojects/fantomas/) project.
6 |
7 | ## Features
8 |
9 | - Format Document
10 | - Format Selection
11 | - Format On Save
12 | - Supports editorconfig
13 | - Patching in formatting changes
14 | - Fast, in-process formatting
15 | - Fully asynchronous, does not increase project load times
16 | - Loaded when required
17 | - Respects key-bindings
18 |
19 | ## Options
20 |
21 | This extension supports [editor config](https://editorconfig.org), for documentation of the options see [Fantomas documentation](https://github.com/fsprojects/fantomas/blob/master/docs/Documentation.md#configuration).
22 |
23 | If no settings can be found for a file, the user profile's formatting options are used.
24 | These can be configured from `F# Tools > Formatting`
25 |
26 | 
27 |
28 | ## Usage
29 |
30 | Any custom key-bindings for Format Document/Selection are respected. The default key-bindings are `ctrl-k + ctrl-d` for document formatting and `ctrl-k + ctrl-f` for formatting a selection.
31 |
32 | 
33 |
34 | ## See an issue?
35 |
36 | If you see an issue with the Visual Studio integration or with configuration, please [file it here](https://github.com/deviousasti/fsharp-formatting-for-vs/issues).
37 |
38 | ## License
39 |
40 | F# Formatting is available under the [MIT license](https://mit-license.org).
--------------------------------------------------------------------------------
/build.ps1:
--------------------------------------------------------------------------------
1 | del .\src\bin\* -Recurse
2 | # npm i -g markdown-to-html-cli
3 | markdown-to-html-cli --source '.\Release Notes.md' --output src/Resources/ReleaseNotes.html
4 | msbuild .\src\FantomasVs.sln -p:Configuration=Release
5 | copy .\src\FantomasVs.VS2019\bin\Release\FantomasVs.vsix .\FantomasVs.VS2019.vsix
6 | copy .\src\FantomasVs.VS2022\bin\Release\FantomasVs.vsix .\FantomasVs.VS2022.vsix
--------------------------------------------------------------------------------
/src/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | #Ignore thumbnails created by Windows
3 | Thumbs.db
4 | #Ignore files built by Visual Studio
5 | *.obj
6 | *.exe
7 | *.pdb
8 | *.user
9 | *.aps
10 | *.pch
11 | *.vspscc
12 | *_i.c
13 | *_p.c
14 | *.ncb
15 | *.suo
16 | *.tlb
17 | *.tlh
18 | *.bak
19 | *.cache
20 | *.ilk
21 | *.log
22 | [Bb]in
23 | [Dd]ebug*/
24 | *.lib
25 | *.sbr
26 | obj/
27 | [Rr]elease*/
28 | _ReSharper*/
29 | [Tt]est[Rr]esult*
30 | .vs/
31 | #Nuget packages folder
32 | packages/
33 |
--------------------------------------------------------------------------------
/src/FantomasVs.Shared/ContentTypeNames.cs:
--------------------------------------------------------------------------------
1 | namespace FantomasVs
2 | {
3 | internal static class ContentTypeNames
4 | {
5 | public const string FSharpContentType = "F#";
6 | }
7 |
8 | }
9 |
--------------------------------------------------------------------------------
/src/FantomasVs.Shared/CustomLineChunker.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using DiffPlex;
3 | using System.Collections.Generic;
4 |
5 | namespace FantomasVs
6 | {
7 | public class AgnosticChunker : IChunker
8 | {
9 | ///
10 | /// Gets the default singleton instance of the chunker.
11 | ///
12 | public static AgnosticChunker Instance { get; } = new AgnosticChunker();
13 |
14 | public string[] Chunk(string text)
15 | {
16 | if (string.IsNullOrEmpty(text))
17 | return Array.Empty();
18 |
19 | var output = new List(128);
20 |
21 | int lastPosition = 0, currentPosition = 0;
22 |
23 | while (currentPosition < text.Length)
24 | {
25 | char ch = text[currentPosition];
26 | switch (ch)
27 | {
28 | case '\n':
29 | case '\r':
30 | {
31 |
32 | if (ch == '\r' && currentPosition < text.Length && text[currentPosition + 1] == '\n')
33 | currentPosition += 2;
34 | else
35 | currentPosition += 1;
36 |
37 | var str = text.Substring(lastPosition, currentPosition - lastPosition);
38 | output.Add(str);
39 |
40 | lastPosition = currentPosition;
41 | break;
42 | }
43 |
44 | default:
45 | {
46 | currentPosition += 1;
47 | break;
48 | }
49 | }
50 | }
51 |
52 | if (lastPosition != text.Length)
53 | {
54 | var str = text.Substring(lastPosition, text.Length - lastPosition);
55 | output.Add(str);
56 | }
57 |
58 | return output.ToArray();
59 | }
60 | }
61 | }
62 |
63 |
--------------------------------------------------------------------------------
/src/FantomasVs.Shared/FantomasHandler.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using DiffPlex;
3 | using System.ComponentModel.Composition;
4 | using System.Diagnostics;
5 | using System.Linq;
6 | using System.Threading;
7 | using System.Threading.Tasks;
8 |
9 | using Microsoft.VisualStudio.Commanding;
10 | using Microsoft.VisualStudio.Text;
11 | using Microsoft.VisualStudio.Text.Editor.Commanding;
12 | using Microsoft.VisualStudio.Text.Editor.Commanding.Commands;
13 | using Microsoft.VisualStudio.Utilities;
14 | using Microsoft.VisualStudio;
15 | using ThreadHelper = Microsoft.VisualStudio.Shell.ThreadHelper;
16 | using ThreadedWaitDialogHelper = Microsoft.VisualStudio.Shell.ThreadedWaitDialogHelper;
17 |
18 | using Fantomas.Client;
19 | using FantomasResponseCode = Fantomas.Client.LSPFantomasServiceTypes.FantomasResponseCode;
20 | using Microsoft.VisualStudio.Threading;
21 |
22 | namespace FantomasVs
23 | {
24 | [Export]
25 | [Export(typeof(ICommandHandler))]
26 | [ContentType(ContentTypeNames.FSharpContentType)]
27 | [Name(PredefinedCommandHandlerNames.FormatDocument)]
28 | [Order(After = PredefinedCommandHandlerNames.Rename)]
29 | public partial class FantomasHandler :
30 | ICommandHandler,
31 | ICommandHandler,
32 | ICommandHandler
33 | {
34 | public string DisplayName => "Automatic Formatting";
35 |
36 | #region Patching
37 |
38 | protected bool ReplaceAll(Span span, ITextBuffer buffer, string oldText, string newText)
39 | {
40 | if (oldText == newText)
41 | return false;
42 |
43 | buffer.Replace(span, newText);
44 | return true;
45 | }
46 |
47 | private (int, int) ShrinkDiff(string currentText, string replaceWith)
48 | {
49 | int startOffset = 0, endOffset = 0;
50 | var currentLength = currentText.Length;
51 | var replaceLength = replaceWith.Length;
52 |
53 | var length = Math.Min(currentLength, replaceLength);
54 |
55 | for (int i = 0; i < length; i++)
56 | {
57 | if (currentText[i] == replaceWith[i])
58 | startOffset++;
59 | else
60 | break;
61 | }
62 |
63 | for (int i = 1; i < length; i++)
64 | {
65 | if ((startOffset + endOffset) >= length)
66 | break;
67 |
68 | if (currentText[currentLength - i] == replaceWith[replaceLength - i])
69 | endOffset++;
70 | else
71 | break;
72 | }
73 |
74 | return (startOffset, endOffset);
75 | }
76 |
77 | protected bool DiffPatch(Span span, ITextBuffer buffer, string oldText, string newText)
78 | {
79 | var snapshot = buffer.CurrentSnapshot;
80 |
81 | using var edit = buffer.CreateEdit();
82 | var diff = Differ.Instance.CreateDiffs(oldText, newText, false, false, AgnosticChunker.Instance);
83 | var lineOffset = snapshot.GetLineNumberFromPosition(span.Start);
84 |
85 | int StartOf(int line) =>
86 | snapshot
87 | .GetLineFromLineNumber(line)
88 | .Start
89 | .Position;
90 |
91 | foreach (var current in diff.DiffBlocks)
92 | {
93 | var start = lineOffset + current.DeleteStartA;
94 |
95 | if (current.DeleteCountA == current.InsertCountB &&
96 | (current.DeleteStartA + current.DeleteCountA) < snapshot.LineCount)
97 | {
98 | var count = current.InsertCountB;
99 | var lstart = StartOf(start);
100 | var lend = StartOf(start + count);
101 | var currentText = snapshot.GetText(lstart, lend - lstart);
102 | var replaceWith = count == 1 ?
103 | diff.PiecesNew[current.InsertStartB] :
104 | string.Join("", diff.PiecesNew, current.InsertStartB, current.InsertCountB);
105 | var (startOffset, endOffset) = ShrinkDiff(currentText, replaceWith);
106 | var totalOffset = startOffset + endOffset;
107 |
108 | var minReplaceWith = replaceWith.Substring(startOffset, replaceWith.Length - totalOffset);
109 |
110 | edit.Replace(lstart + startOffset, Math.Max(0, lend - lstart - totalOffset), minReplaceWith);
111 | }
112 | else
113 | {
114 |
115 | for (int i = 0; i < current.DeleteCountA; i++)
116 | {
117 | var ln = snapshot.GetLineFromLineNumber(start + i);
118 | edit.Delete(ln.Start, ln.LengthIncludingLineBreak);
119 | }
120 |
121 | for (int i = 0; i < current.InsertCountB; i++)
122 | {
123 | var ln = snapshot.GetLineFromLineNumber(start);
124 | edit.Insert(ln.Start, diff.PiecesNew[current.InsertStartB + i]);
125 | }
126 | }
127 | }
128 |
129 | edit.Apply();
130 |
131 | return diff.DiffBlocks.Any();
132 | }
133 |
134 | #endregion
135 |
136 | #region Formatting
137 |
138 | public enum FormatKind
139 | {
140 | Document,
141 | Selection,
142 | IsolatedSelection
143 | }
144 |
145 | public bool CommandHandled => true;
146 |
147 | public async Task FormatAsync(SnapshotSpan vspan, EditorCommandArgs args, CommandExecutionContext context, FormatKind kind)
148 | {
149 | var token = context.OperationContext.UserCancellationToken;
150 | var instance = await FantomasVsPackage.Instance.WithCancellation(token);
151 |
152 | await SetStatusAsync("Formatting...", instance, token);
153 | await Task.Yield();
154 |
155 | var buffer = args.TextView.TextBuffer;
156 | var caret = args.TextView.Caret.Position;
157 |
158 | var service = instance.FantomasService;
159 | var fantopts = instance.Options;
160 | var document = buffer.Properties.GetProperty(typeof(ITextDocument));
161 | var path = document.FilePath;
162 | var workingDir = System.IO.Path.GetDirectoryName(path);
163 | var hasDiff = false;
164 | var hasError = false;
165 |
166 | try
167 | {
168 | var originText = kind switch
169 | {
170 | FormatKind.Document => buffer.CurrentSnapshot.GetText(),
171 | FormatKind.Selection => buffer.CurrentSnapshot.GetText(),
172 | FormatKind.IsolatedSelection => vspan.GetText(),
173 | _ => throw new NotSupportedException($"Operation {kind} is not supported")
174 | };
175 | var response = await (kind switch
176 | {
177 | FormatKind.Document or FormatKind.IsolatedSelection =>
178 | service.FormatDocumentAsync(new Contracts.FormatDocumentRequest(originText, path, null, MakeCursorPosition(caret.BufferPosition)), token),
179 | FormatKind.Selection =>
180 | service.FormatSelectionAsync(new Contracts.FormatSelectionRequest(originText, path, null, MakeRange(vspan, path)), token),
181 | _ => throw new NotSupportedException($"Operation {kind} is not supported")
182 | });
183 |
184 | switch ((FantomasResponseCode)response.Code)
185 | {
186 | case FantomasResponseCode.Formatted:
187 | {
188 | var newText = response.Content.Value;
189 | var oldText = vspan.GetText();
190 |
191 | if (fantopts.ApplyDiff)
192 | {
193 | hasDiff = DiffPatch(vspan, buffer, oldText, newText);
194 | }
195 | else
196 | {
197 | hasDiff = ReplaceAll(vspan, buffer, oldText, newText);
198 | }
199 | break;
200 | }
201 | case FantomasResponseCode.UnChanged:
202 | case FantomasResponseCode.Ignored:
203 | break;
204 | case FantomasResponseCode.ToolNotFound:
205 | {
206 | var view = new InstallChoiceWindow();
207 | var result = await InstallAsync(view.GetDialogAction(), workingDir, token);
208 | switch (result)
209 | {
210 | case InstallResult.Succeded:
211 | {
212 | InstallResultDialog.ShowDialog("Fantomas Tool was succesfully installed!");
213 | using (var session = ThreadedWaitDialogHelper.StartWaitDialog(instance.DialogFactory, "Starting instance..."))
214 | {
215 | await FormatAsync(vspan, args, context, kind);
216 | }
217 | break;
218 | }
219 | case InstallResult.Failed:
220 | {
221 | hasError = true;
222 | InstallResultDialog.ShowDialog("Fantomas Tool could not be installed. You may not have a tool manifest set up. Please check the log for details.");
223 | await FocusLogAsync(token);
224 | break;
225 | }
226 | }
227 |
228 | break;
229 | }
230 |
231 | case FantomasResponseCode.Error:
232 | case FantomasResponseCode.FileNotFound:
233 | case FantomasResponseCode.FilePathIsNotAbsolute:
234 | {
235 | hasError = true;
236 | var error = response.Content.Value;
237 | await SetStatusAsync($"Could not format: {error.Replace(path, "")}", instance, token);
238 | await WriteLogAsync(error, token);
239 | await FocusLogAsync(token);
240 | break;
241 | }
242 | case FantomasResponseCode.DaemonCreationFailed:
243 | {
244 | await WriteLogAsync($"Creating the Fantomas Daemon failed:\n{response.Content?.Value}", token);
245 | await FocusLogAsync(token);
246 | hasError = true;
247 | break;
248 | }
249 | default:
250 | throw new NotSupportedException($"The {nameof(FantomasResponseCode)} value '{response.Code}' is unexpected.\n Error: {response.Content?.Value}");
251 | }
252 |
253 | if(hasError)
254 | {
255 | await WriteLogAsync("Attempting to find Fantomas Tool...", token);
256 | var folder = LSPFantomasServiceTypes.Folder.NewFolder(workingDir);
257 | var toolLocation = FantomasToolLocator.findFantomasTool(folder);
258 | var result = toolLocation.IsError ? $"Failed to find tool: {toolLocation.ErrorValue}" : $"Found at: {toolLocation.ResultValue}";
259 | await WriteLogAsync(result, token);
260 | }
261 | }
262 | catch (NotSupportedException ex)
263 | {
264 | await WriteLogAsync($"The operation is not supported:\n {ex.Message}", token);
265 | }
266 | catch (Exception ex)
267 | {
268 | hasError = true;
269 | await WriteLogAsync($"The formatting operation failed:\n {ex}", token);
270 | await SetStatusAsync($"Could not format: {ex.Message.Replace(path, "")}", instance, token);
271 | }
272 |
273 | args.TextView.Caret.MoveTo(
274 | caret
275 | .BufferPosition
276 | .TranslateTo(buffer.CurrentSnapshot, PointTrackingMode.Positive)
277 | );
278 |
279 | if (kind == FormatKind.Selection || kind == FormatKind.IsolatedSelection)
280 | args.TextView.Selection.Select(
281 | vspan.TranslateTo(args.TextView.TextSnapshot, SpanTrackingMode.EdgeInclusive),
282 | false);
283 |
284 | if (hasError) await Task.Delay(2000);
285 | await SetStatusAsync("Ready.", instance, token);
286 |
287 | return hasDiff;
288 | }
289 |
290 | protected async Task<(bool, string)> RunProcessAsync(string name, string args, string workingDir, CancellationToken token)
291 | {
292 | var startInfo = new ProcessStartInfo
293 | {
294 | FileName = name,
295 | Arguments = args,
296 | UseShellExecute = false,
297 | RedirectStandardOutput = true,
298 | RedirectStandardError = true,
299 | CreateNoWindow = true,
300 | WorkingDirectory = workingDir
301 | };
302 |
303 | try
304 | {
305 | using var process = Process.Start(startInfo);
306 | var exitCode = await process.WaitForExitAsync(token);
307 |
308 | token.ThrowIfCancellationRequested();
309 |
310 | var output = exitCode switch
311 | {
312 | 0 => await process.StandardOutput.ReadToEndAsync().WithCancellation(token),
313 | _ => await process.StandardError.ReadToEndAsync().WithCancellation(token)
314 | };
315 |
316 | return (exitCode == 0, output);
317 | }
318 | catch (Exception ex)
319 | {
320 | return (false, $"Failed to run dotnet tool {ex}");
321 | }
322 | }
323 |
324 | public async Task InstallAsync(InstallAction installAction, string workingDir, CancellationToken token)
325 | {
326 | async Task LaunchUrl(string uri)
327 | {
328 | try
329 | {
330 | Process.Start(new ProcessStartInfo(uri) { UseShellExecute = true });
331 | }
332 | catch (Exception ex)
333 | {
334 | await WriteLogAsync($"Failed to launch url: {uri}\n{ex}", token);
335 | }
336 |
337 | return InstallResult.Skipped;
338 | }
339 |
340 | async Task LaunchDotnet(string caption, string args)
341 | {
342 | await WriteLogAsync(caption, token);
343 | await WriteLogAsync("Running dotnet installation...", token);
344 | await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(token);
345 | var instance = await FantomasVsPackage.Instance;
346 | using var session = ThreadedWaitDialogHelper.StartWaitDialog(instance.DialogFactory, caption);
347 | var (success, output) = await RunProcessAsync("dotnet", args, workingDir, session.UserCancellationToken);
348 | await WriteLogAsync(output, token);
349 | return success ? InstallResult.Succeded : InstallResult.Failed;
350 | }
351 |
352 | return installAction switch
353 | {
354 | InstallAction.Global => await LaunchDotnet("Installing tool globally", "tool install --verbosity normal --global fantomas"),
355 | InstallAction.Local => await LaunchDotnet("Installing tool locally", "tool install --verbosity normal fantomas"),
356 | InstallAction.ShowDocs => await LaunchUrl("https://fsprojects.github.io/fantomas/docs/index.html"),
357 | _ => InstallResult.Skipped, // do nothing
358 | };
359 | }
360 |
361 | public static Contracts.FormatCursorPosition MakeCursorPosition(SnapshotPoint point)
362 | {
363 | var line = point.GetContainingLine();
364 | var startCol = Math.Max(0, point.Position - line.Start.Position - 1);
365 | return new Contracts.FormatCursorPosition(line.LineNumber + 1, startCol);
366 | }
367 |
368 | public static Contracts.FormatSelectionRange MakeRange(SnapshotSpan vspan, string path)
369 | {
370 | // Beware that the range argument is inclusive.
371 | // If the range has a trailing newline, it will appear in the formatted result.
372 |
373 | var start = vspan.Start.GetContainingLine();
374 | var end = vspan.End.GetContainingLine();
375 | var startLine = start.LineNumber + 1;
376 | var startCol = Math.Max(0, vspan.Start.Position - start.Start.Position - 1);
377 | var endLine = end.LineNumber + 1;
378 | var endCol = Math.Max(0, vspan.End.Position - end.Start.Position - 1);
379 |
380 | var range = new Contracts.FormatSelectionRange(
381 | startLine: startLine,
382 | startColumn: startCol,
383 | endLine: endLine,
384 | endColumn: endCol);
385 | return range;
386 | }
387 |
388 | public Task FormatAsync(EditorCommandArgs args, CommandExecutionContext context)
389 | {
390 | var snapshot = args.TextView.TextSnapshot;
391 | var vspan = new SnapshotSpan(snapshot, new Span(0, snapshot.Length));
392 | return FormatAsync(vspan, args, context, FormatKind.Document);
393 | }
394 |
395 | protected async Task SetStatusAsync(string text, FantomasVsPackage instance, CancellationToken token)
396 | {
397 | await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(token);
398 | var statusBar = instance.Statusbar;
399 | // Make sure the status bar is not frozen
400 |
401 | if (statusBar.IsFrozen(out var frozen) == VSConstants.S_OK && frozen != 0)
402 | statusBar.FreezeOutput(0);
403 |
404 | // Set the status bar text and make its display static.
405 | statusBar.SetText(text);
406 | }
407 |
408 | #endregion
409 |
410 | #region Output Window
411 |
412 | public OuptutLogging Logging { get; } = new();
413 |
414 | public Task WriteLogAsync(string text, CancellationToken token) => Logging.LogTextAsync(text, token);
415 |
416 | public Task FocusLogAsync(CancellationToken token) => Logging.BringToFrontAsync(token);
417 |
418 | #endregion
419 |
420 | #region Logging
421 |
422 | protected void LogTask(Task task)
423 | {
424 | var _ = task.ContinueWith(async t =>
425 | {
426 | if (t.IsFaulted)
427 | await WriteLogAsync(t.Exception.ToString(), CancellationToken.None);
428 |
429 | }, TaskScheduler.Default);
430 | }
431 |
432 | #endregion
433 |
434 | #region Format Document
435 |
436 | public bool ExecuteCommand(FormatDocumentCommandArgs args, CommandExecutionContext executionContext)
437 | {
438 | LogTask(FormatAsync(args, executionContext));
439 | return CommandHandled;
440 | }
441 |
442 | public CommandState GetCommandState(FormatDocumentCommandArgs args)
443 | {
444 | return args.TextView.IsClosed ? CommandState.Unavailable : CommandState.Available;
445 | }
446 |
447 | #endregion
448 |
449 | #region Format Selection
450 |
451 | public CommandState GetCommandState(FormatSelectionCommandArgs args)
452 | {
453 | return args.TextView.Selection.IsEmpty ? CommandState.Unavailable : CommandState.Available;
454 | }
455 |
456 | public bool ExecuteCommand(FormatSelectionCommandArgs args, CommandExecutionContext executionContext)
457 | {
458 | var selections = args.TextView.Selection.SelectedSpans;
459 |
460 | // This command shouldn't be called
461 | // if there's no selection, but it's bad practice
462 | // to surface exceptions to VS
463 | if (selections.Count == 0)
464 | return false;
465 |
466 | var vspan = new SnapshotSpan(args.TextView.TextSnapshot, selections.Single().Span);
467 | LogTask(FormatAsync(vspan, args, executionContext, FormatKind.Selection));
468 | return CommandHandled;
469 | }
470 |
471 | #endregion
472 |
473 | #region Format On Save
474 |
475 | public CommandState GetCommandState(SaveCommandArgs args)
476 | {
477 | return CommandState.Unavailable;
478 | }
479 |
480 | public bool ExecuteCommand(SaveCommandArgs args, CommandExecutionContext executionContext)
481 | {
482 | LogTask(FormatOnSaveAsync(args, executionContext));
483 | return false;
484 | }
485 |
486 | protected async Task FormatOnSaveAsync(SaveCommandArgs args, CommandExecutionContext executionContext)
487 | {
488 | var instance = await FantomasVsPackage.Instance;
489 | if (!instance.Options.FormatOnSave)
490 | return;
491 |
492 | var hasDiff = await FormatAsync(args, executionContext);
493 |
494 | if (!hasDiff || !instance.Options.CommitChanges)
495 | return;
496 |
497 | var buffer = args.SubjectBuffer;
498 | var document = buffer.Properties.GetProperty(typeof(ITextDocument));
499 |
500 | document?.Save();
501 | }
502 |
503 | #endregion
504 | }
505 | }
506 |
507 |
--------------------------------------------------------------------------------
/src/FantomasVs.Shared/FantomasOptionsPage.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.ComponentModel;
3 | using Microsoft.VisualStudio.Shell;
4 | using System.Runtime.InteropServices;
5 |
6 | namespace FantomasVs
7 | {
8 | [Guid(GuidString)]
9 | public class FantomasOptionsPage : DialogPage
10 | {
11 | public const string GuidString = "74927147-72e8-4b47-a70d-5568807d6878";
12 |
13 | #region Performance
14 |
15 | [Category("Performance")]
16 | [DisplayName("Apply As Diff")]
17 | [Description("Applies the formatting as changes, which shows which lines were changed. Turn off if computing the diff is too slow. ")]
18 | public bool ApplyDiff { get; set; } = true;
19 |
20 | [Category("Performance")]
21 | [DisplayName("Enable SpaceBar Heating")]
22 | [Description("xkcd/1172")]
23 | public bool EnableSpaceBarHeating { get; set; } = false;
24 |
25 | #endregion
26 |
27 | #region On Save
28 |
29 | [Category("On Save")]
30 | [DisplayName("Format On Save")]
31 | [Description("This triggers a formatting whenever you hit save")]
32 | public bool FormatOnSave { get; set; } = false;
33 |
34 | [Category("On Save")]
35 | [DisplayName("Commit Changes")]
36 | [Description("Set this to false if you don't want to commit formatting changes to the file unless you hit save once again")]
37 | public bool CommitChanges { get; set; } = true;
38 |
39 | #endregion
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/FantomasVs.Shared/FantomasVs.Shared.projitems:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | $(MSBuildAllProjects);$(MSBuildThisFileFullPath)
5 | true
6 | b7a936ca-e24e-4ea6-9571-1373c6e44614
7 |
8 |
9 | FantomasVs.Shared
10 |
11 |
12 |
13 |
14 |
15 |
16 | Component
17 |
18 |
19 |
20 | InstallChoice.xaml
21 |
22 |
23 | InstallResultDialog.xaml
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 | MSBuild:Compile
35 | Designer
36 |
37 |
38 | MSBuild:Compile
39 | Designer
40 |
41 |
42 |
--------------------------------------------------------------------------------
/src/FantomasVs.Shared/FantomasVs.Shared.shproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | b7a936ca-e24e-4ea6-9571-1373c6e44614
5 | 14.0
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/FantomasVs.Shared/FantomasVsPackage.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Diagnostics;
3 | using System.Runtime.InteropServices;
4 | using System.Threading;
5 | using System.Threading.Tasks;
6 | using Fantomas.Client;
7 | using Microsoft.VisualStudio.ComponentModelHost;
8 | using Microsoft.VisualStudio.Shell;
9 | using Microsoft.VisualStudio.Shell.Interop;
10 | using Task = System.Threading.Tasks.Task;
11 |
12 | namespace FantomasVs
13 | {
14 |
15 | // DO NOT REMOVE THIS MAGICAL INCANTATION NO MATTER HOW MUCH VS WARNS YOU OF DEPRECATION
16 | // --------------------------------------------------------------------------------------
17 | [InstalledProductRegistration("F# Formatting", "F# source code formatting using Fantomas.", "1.0", IconResourceID = 400)]
18 | // --------------------------------------------------------------------------------------
19 |
20 | // Package registration attributes
21 | [PackageRegistration(UseManagedResourcesOnly = true, AllowsBackgroundLoading = true)]
22 | [Guid(FantomasVsPackage.PackageGuidString)]
23 |
24 | // Auto load only if a solution is open, this is important too
25 | [ProvideAutoLoad(UIContextGuids80.SolutionExists, PackageAutoLoadFlags.BackgroundLoad)]
26 |
27 | // Options page
28 | [ProvideOptionPage(typeof(FantomasOptionsPage), "F# Tools", "Formatting", 0, 0, supportsAutomation: true)]
29 |
30 | public sealed partial class FantomasVsPackage : AsyncPackage
31 | {
32 | ///
33 | /// FantomasVsPackage GUID string.
34 | ///
35 | public const string PackageGuidString = "74927147-72e8-4b47-a80d-5568807d6878";
36 |
37 | private static readonly TaskCompletionSource _instance = new();
38 | public static Task Instance => _instance.Task;
39 |
40 | public FantomasOptionsPage Options => GetDialogPage(typeof(FantomasOptionsPage)) as FantomasOptionsPage ?? new FantomasOptionsPage();
41 |
42 | public IVsStatusbar Statusbar { get; private set; }
43 | public IVsOutputWindow OutputPane { get; private set; }
44 | public IVsThreadedWaitDialogFactory DialogFactory { get; private set; }
45 |
46 | private Lazy _fantomasService = new (() => new LSPFantomasService.LSPFantomasService());
47 | public Contracts.FantomasService FantomasService => _fantomasService.Value;
48 |
49 | #region Package Members
50 |
51 | ///
52 | /// Initialization of the package; this method is called right after the package is sited, so this is the place
53 | /// where you can put all the initialization code that rely on services provided by VisualStudio.
54 | ///
55 | /// A cancellation token to monitor for initialization cancellation, which can occur when VS is shutting down.
56 | /// A provider for progress updates.
57 | /// A task representing the async work of package initialization, or an already completed task if there is none. Do not return null from this method.
58 | protected override async Task InitializeAsync(CancellationToken cancellationToken, IProgress progress)
59 | {
60 | Trace.WriteLine("Fantomas Vs Package Loaded");
61 |
62 | Statusbar = await this.GetServiceAsync();
63 | OutputPane = await this.GetServiceAsync();
64 | DialogFactory = await this.GetServiceAsync();
65 |
66 | // signal that package is ready
67 | _instance.SetResult(this);
68 |
69 | // When initialized asynchronously, the current thread may be a background thread at this point.
70 | // Do any initialization that requires the UI thread after switching to the UI thread.
71 | await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
72 |
73 | }
74 |
75 | protected override void Dispose(bool disposing)
76 | {
77 | if (disposing)
78 | {
79 | try
80 | {
81 | FantomasService.Dispose();
82 | Trace.WriteLine("Fantomas Vs Package Disposed");
83 | }
84 | catch (Exception ex)
85 | {
86 | Trace.WriteLine(ex);
87 | }
88 | }
89 |
90 | base.Dispose(disposing);
91 | }
92 |
93 | #endregion
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/src/FantomasVs.Shared/InstallChoice.xaml:
--------------------------------------------------------------------------------
1 |
17 |
18 |
30 |
33 |
40 |
41 |
42 |
43 | An installation of the F# source code formatter, Fantomas could not be found.
44 |
45 |
46 | You can choose to install it:
47 |
48 |
49 |
61 |
73 |
84 |
85 |
86 | You can read the documentation to find out more.
87 |
88 |
89 |
90 |
--------------------------------------------------------------------------------
/src/FantomasVs.Shared/InstallChoice.xaml.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.VisualStudio.PlatformUI;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.Diagnostics;
5 | using System.Linq;
6 | using System.Text;
7 | using System.Threading.Tasks;
8 | using System.Windows;
9 | using System.Windows.Controls;
10 | using System.Windows.Data;
11 | using System.Windows.Documents;
12 | using System.Windows.Input;
13 | using System.Windows.Media;
14 | using System.Windows.Media.Imaging;
15 | using System.Windows.Navigation;
16 | using System.Windows.Shapes;
17 |
18 | namespace FantomasVs
19 | {
20 | public enum InstallAction
21 | {
22 | None,
23 | Global,
24 | Local,
25 | UpdateGlobal,
26 | UpdateLocal,
27 | ShowDocs
28 | }
29 |
30 | public enum InstallResult
31 | {
32 | Skipped,
33 | Succeded,
34 | Failed
35 | }
36 |
37 | ///
38 | /// Interaction logic for InstallChoiceWindow.xaml
39 | ///
40 | public partial class InstallChoiceWindow : DialogWindow
41 | {
42 | class ChoiceCommand : ICommand
43 | {
44 | public Action