├── .gitignore
├── .gitmodules
├── Readme.md
├── Release Notes.md
├── build.ps1
└── src
├── ContentTypeNames.cs
├── FSharpLintVs.csproj
├── FSharpLintVs.sln
├── FsLint
└── FSharpLint.Core.fsproj
├── FsLintVsPackage.cs
├── LintChecker.cs
├── LintCheckerProvider.cs
├── LintError.cs
├── LintProjectInfo.cs
├── LintTagger.cs
├── Options
└── FsLintOptionsPage.cs
├── Properties
└── AssemblyInfo.cs
├── Resources
├── License.txt
├── ReleaseNotes.html
└── logo.png
├── SubscriptionManager.cs
├── SuggestionPreview.xaml
├── SuggestionPreview.xaml.cs
├── Suggestions
├── LintActionsSource.cs
├── LintFixAction.cs
├── LintSuggestionProvider.cs
├── LintSuppressAction.cs
└── LintSuppressBy.cs
├── Table
├── LintTableSnapshotFactory.cs
└── LintingErrorsSnapshot.cs
└── source.extension.vsixmanifest
/.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 |
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "fsharplint"]
2 | path = fsharplint
3 | url = https://github.com/deviousasti/FSharpLint/
4 | branch = master
5 |
--------------------------------------------------------------------------------
/Readme.md:
--------------------------------------------------------------------------------
1 | # Linting For F#
2 |
3 | This is a full-featured Visual Studio integration for the [**FSharpLint**](https://github.com/fsprojects/FSharpLint) project.
4 |
5 | ## Features
6 |
7 | - Live error tagging
8 | - Shows warnings in Quick Info tooltip
9 | - Filterable warnings in errors panel
10 | - Links to issues
11 | - Code fixes
12 | - Warning suppression in smart actions
13 | - Fast, in-process linting
14 | - Fully asynchronous
15 |
16 | ## Configuration
17 |
18 | We look for a `fsharplint.json` in this order:
19 |
20 | - In the same folder as the current document
21 | - In the project (`.fsproj`) directory
22 | - In the solution (`.sln`) directory
23 | - In the directory above the solution directory, if it exists
24 |
25 | ## In action
26 |
27 | 
28 |
29 | ## Options
30 |
31 | Linter options can be found in `F# Tools > Linting`
32 |
33 | 
34 |
35 | ## See an issue?
36 |
37 | If you see an issue with the Visual Studio integration or with configuration, please [file it here](https://github.com/deviousasti/fsharp-linting-for-vs/issues).
38 |
39 | ## License
40 |
41 | This project is licensed under the MIT license, a copy of which can be found [here](src/Resources/License.txt).
42 |
--------------------------------------------------------------------------------
/Release Notes.md:
--------------------------------------------------------------------------------
1 | Release Notes
2 | ============
3 |
4 | 0.6
5 | ----
6 |
7 | * Upgrade to FCS 39
8 | * Custom build of FSharpLint 0.19
9 |
10 | 0.5.2
11 | ----
12 |
13 | * Check for config in parent folder of solution (@kjreills)
14 |
15 | 0.5.1
16 | ----
17 |
18 | * Hotfix release
19 |
20 | 0.5
21 | ----
22 |
23 | * Support new rules
24 | * Update to FCS 38.0.2
25 |
26 | 0.4
27 | ----
28 |
29 | * Hotfix for nested statements
30 |
31 | 0.3
32 | ----
33 |
34 | * Lints TAST
35 | * Support type-checked linting enabled by options
36 | * Make latency configurable
37 |
38 | 0.2
39 | ----
40 |
41 | * Look for JSON configuration in related paths (see Readme)
42 | * Use throttle to limit linting calls
43 |
44 | 0.1
45 | ----
46 |
47 | * Lints AST
48 | * Displays error list
49 | * Tags warnings in text view
50 | * Code Fix: Replace
51 | * Code Fix: Suppress Issues
52 | * Above
53 | * In-line
54 | * In section
55 |
--------------------------------------------------------------------------------
/build.ps1:
--------------------------------------------------------------------------------
1 | del .\src\bin\* -Recurse
2 | markdown '.\Release Notes.md' > src\Resources\ReleaseNotes.html
3 | msbuild .\src\FSharpLintVs.sln -p:Configuration=Release
4 | copy .\src\bin\Release\FSharpLintVs.vsix .\
--------------------------------------------------------------------------------
/src/ContentTypeNames.cs:
--------------------------------------------------------------------------------
1 | namespace FSharpLintVs
2 | {
3 | public static class ContentTypeNames
4 | {
5 | public const string FSharpContentType = "F#";
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/src/FSharpLintVs.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 16.0
5 | $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)
6 |
7 |
8 |
9 |
10 | Debug
11 | AnyCPU
12 | 2.0
13 | {82b43b9b-a64c-4715-b499-d71e9ca2bd60};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}
14 | {37577282-1289-40DB-AD3D-24499BD09DAE}
15 | Library
16 | Properties
17 | 8.0
18 | en
19 | FSharpLintVs
20 | FSharpLintVs
21 | v4.8
22 | true
23 | true
24 | true
25 | false
26 | false
27 | true
28 | true
29 | Program
30 | $(DevEnvDir)devenv.exe
31 | /rootsuffix Exp
32 |
33 |
34 | true
35 | full
36 | false
37 | bin\Debug\
38 | DEBUG;TRACE
39 | prompt
40 | 4
41 |
42 |
43 | pdbonly
44 | true
45 | bin\Release\
46 | TRACE
47 | prompt
48 | 4
49 |
50 |
51 |
52 |
53 |
54 |
55 | Component
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 | SuggestionPreview.xaml
72 |
73 |
74 |
75 |
76 | Always
77 | true
78 |
79 |
80 | Always
81 | true
82 |
83 |
84 | Designer
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 | 39.0.0
103 |
104 |
105 | 5.0.1
106 |
107 |
108 | compile; build; native; contentfiles; analyzers; buildtransitive
109 |
110 |
111 | runtime; build; native; contentfiles; analyzers; buildtransitive
112 | all
113 |
114 |
115 | 16.2.29116.78
116 |
117 |
118 |
119 |
120 | Always
121 | true
122 |
123 |
124 |
125 |
126 | {7857e9a7-8879-430e-a9b0-a11dcf2ef68d}
127 | FSharpLint.Core
128 |
129 |
130 |
131 |
132 | Designer
133 | MSBuild:Compile
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
149 |
--------------------------------------------------------------------------------
/src/FSharpLintVs.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 16
4 | VisualStudioVersion = 16.0.30223.230
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FSharpLintVs", "FSharpLintVs.csproj", "{37577282-1289-40DB-AD3D-24499BD09DAE}"
7 | EndProject
8 | Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "FSharpLint.Core", "FsLint\FSharpLint.Core.fsproj", "{7857E9A7-8879-430E-A9B0-A11DCF2EF68D}"
9 | EndProject
10 | Global
11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
12 | Debug|Any CPU = Debug|Any CPU
13 | Release|Any CPU = Release|Any CPU
14 | EndGlobalSection
15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
16 | {37577282-1289-40DB-AD3D-24499BD09DAE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
17 | {37577282-1289-40DB-AD3D-24499BD09DAE}.Debug|Any CPU.Build.0 = Debug|Any CPU
18 | {37577282-1289-40DB-AD3D-24499BD09DAE}.Release|Any CPU.ActiveCfg = Release|Any CPU
19 | {37577282-1289-40DB-AD3D-24499BD09DAE}.Release|Any CPU.Build.0 = Release|Any CPU
20 | {7857E9A7-8879-430E-A9B0-A11DCF2EF68D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
21 | {7857E9A7-8879-430E-A9B0-A11DCF2EF68D}.Debug|Any CPU.Build.0 = Debug|Any CPU
22 | {7857E9A7-8879-430E-A9B0-A11DCF2EF68D}.Release|Any CPU.ActiveCfg = Release|Any CPU
23 | {7857E9A7-8879-430E-A9B0-A11DCF2EF68D}.Release|Any CPU.Build.0 = Release|Any CPU
24 | EndGlobalSection
25 | GlobalSection(SolutionProperties) = preSolution
26 | HideSolutionNode = FALSE
27 | EndGlobalSection
28 | GlobalSection(ExtensibilityGlobals) = postSolution
29 | SolutionGuid = {0039EA05-B140-4301-8B82-535FB9B5395C}
30 | EndGlobalSection
31 | EndGlobal
32 |
--------------------------------------------------------------------------------
/src/FsLint/FSharpLint.Core.fsproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net48
5 | true
6 | true
7 | true
8 | FSharpLint.Core
9 | false
10 |
11 | FSharpLint.Core
12 | API to programmatically run FSharpLint.
13 | F#;fsharp;lint;FSharpLint;fslint;api
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 | Always
112 |
113 |
114 | Designer
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
--------------------------------------------------------------------------------
/src/FsLintVsPackage.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Diagnostics;
3 | using System.Runtime.InteropServices;
4 | using System.Threading;
5 | using System.Threading.Tasks;
6 | using EnvDTE80;
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 FSharpLintVs
13 | {
14 | // DO NOT REMOVE THIS MAGICAL INCANTATION NO MATTER HOW MUCH VS WARNS YOU OF DEPRECATION
15 | // --------------------------------------------------------------------------------------
16 | [InstalledProductRegistration("F# Lint", "Source code linting for F#.", "0.6", IconResourceID = 400)]
17 | // --------------------------------------------------------------------------------------
18 |
19 | // Package registration attributes
20 | [PackageRegistration(UseManagedResourcesOnly = true, AllowsBackgroundLoading = true)]
21 | [Guid(FsLintVsPackage.PackageGuidString)]
22 |
23 | // Auto load only if a solution is open, this is important too
24 | [ProvideAutoLoad(UIContextGuids80.SolutionExists, PackageAutoLoadFlags.BackgroundLoad)]
25 |
26 | // Options page
27 | [ProvideOptionPage(typeof(FsLintOptionsPage), "F# Tools", "Linting", 0, 0, supportsAutomation: true)]
28 |
29 | public sealed partial class FsLintVsPackage : AsyncPackage
30 | {
31 | ///
32 | /// FsLintVsPackage GUID string.
33 | ///
34 | public const string PackageGuidString = "74927147-72e8-4b47-a80d-5568807d6879";
35 |
36 | private static readonly TaskCompletionSource _instance = new TaskCompletionSource();
37 | public static Task Instance => _instance.Task;
38 |
39 | private FsLintOptionsPage _fsLintOptions;
40 | public FsLintOptionsPage Options => _fsLintOptions ??= GetDialogPage(typeof(FsLintOptionsPage)) as FsLintOptionsPage;
41 |
42 | public IComponentModel MefHost { get; private set; }
43 |
44 | public IVsStatusbar Statusbar { get; private set; }
45 |
46 | public DTE2 Dte { get; private set; }
47 |
48 | public IVsSolution SolutionService { get; private set; }
49 |
50 | #region Package Members
51 |
52 | ///
53 | /// Initialization of the package; this method is called right after the package is sited, so this is the place
54 | /// where you can put all the initialization code that rely on services provided by VisualStudio.
55 | ///
56 | /// A cancellation token to monitor for initialization cancellation, which can occur when VS is shutting down.
57 | /// A provider for progress updates.
58 | /// 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.
59 | protected override async Task InitializeAsync(CancellationToken cancellationToken, IProgress progress)
60 | {
61 | Trace.WriteLine("F# Lint Vs Package Loaded");
62 |
63 | MefHost = await this.GetServiceAsync();
64 | Statusbar = await this.GetServiceAsync();
65 | Dte = await this.GetServiceAsync();
66 | SolutionService = await this.GetServiceAsync();
67 |
68 | // When initialized asynchronously, the current thread may be a background thread at this point.
69 | // Do any initialization that requires the UI thread after switching to the UI thread.
70 | await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
71 |
72 | // signal that package is ready
73 | _instance.SetResult(this);
74 | }
75 |
76 | #endregion
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/src/LintChecker.cs:
--------------------------------------------------------------------------------
1 | using EnvDTE;
2 | using FSharp.Compiler;
3 | using FSharp.Compiler.SourceCodeServices;
4 | using FSharp.Compiler.Text;
5 | using FSharpLint.Application;
6 | using Microsoft.FSharp.Collections;
7 | using Microsoft.FSharp.Control;
8 | using Microsoft.FSharp.Core;
9 | using Microsoft.VisualStudio;
10 | using Microsoft.VisualStudio.Text;
11 | using Microsoft.VisualStudio.Text.Editor;
12 | using Microsoft.VisualStudio.Threading;
13 | using System;
14 | using System.Collections.Generic;
15 | using System.Diagnostics;
16 | using System.IO;
17 | using System.Linq;
18 | using System.Threading;
19 | using System.Threading.Tasks;
20 |
21 | namespace FSharpLintVs
22 | {
23 | ///
24 | /// Checks the linting errors for a specific buffer.
25 | ///
26 | ///
27 | ///
28 | /// The lifespan of this object is tied to the lifespan of the taggers on the view.
29 | /// On creation of the first tagger, the LintChecker starts doing
30 | /// work to find errors in the file. On the disposal of the last tagger, it shuts down.
31 | ///
32 | ///
33 | /// The checker service does parsing in this class.
34 | ///
35 | ///
36 | public class LintChecker : IDisposable
37 | {
38 | private readonly LintCheckerProvider _provider;
39 | private readonly ITextBuffer _buffer;
40 |
41 | private ITextSnapshot _currentSnapshot;
42 | private NormalizedSnapshotSpanCollection _dirtySpans;
43 | private readonly List _activeTaggers = new List();
44 | private CancellationTokenSource _cts;
45 | private FSharpParsingOptions parseOpts;
46 | private FSharpProjectOptions projectOptions;
47 |
48 | public ITextDocument Document { get; }
49 |
50 | public Task Linting { get; private set; }
51 |
52 | public LintProjectInfo ProjectInfo { get; private set; }
53 |
54 | public Lint.ConfigurationParam Configuration { get; private set; }
55 |
56 | public int RefCount => _activeTaggers.Count;
57 |
58 | public LintingErrorsSnapshot LastErrorsSnapshot { get; private set; }
59 |
60 | public bool HasSnapshot => LastErrorsSnapshot != null;
61 |
62 | public event EventHandler Updated;
63 |
64 | public bool IsDisposed { get; private set; }
65 |
66 | public LintTableSnapshotFactory Factory { get; }
67 |
68 |
69 | public LintChecker(LintCheckerProvider provider, ITextView textView, ITextBuffer buffer)
70 | {
71 | _provider = provider;
72 | _buffer = buffer;
73 | _currentSnapshot = buffer.CurrentSnapshot;
74 |
75 | // Get the name of the underlying document buffer
76 | if (provider.TextDocumentFactoryService.TryGetTextDocument(textView.TextDataModel.DocumentBuffer, out ITextDocument document))
77 | {
78 | this.Document = document;
79 | }
80 |
81 | this.Factory = new LintTableSnapshotFactory(new LintingErrorsSnapshot(document, version: 0));
82 | }
83 |
84 | public void AddTagger(LintTagger tagger)
85 | {
86 | if (RefCount == 0)
87 | {
88 | Initialize();
89 | RunLinter();
90 | }
91 |
92 | _activeTaggers.Add(tagger);
93 | }
94 |
95 | public void Initialize()
96 | {
97 | _buffer.ChangedLowPriority += this.OnBufferChange;
98 |
99 | _dirtySpans = new NormalizedSnapshotSpanCollection(new SnapshotSpan(_currentSnapshot, 0, _currentSnapshot.Length));
100 |
101 | _provider.AddLintChecker(this);
102 | }
103 |
104 | public void RemoveTagger(LintTagger tagger)
105 | {
106 | _activeTaggers.Remove(tagger);
107 |
108 | if (RefCount == 0)
109 | {
110 | Dispose();
111 | }
112 | }
113 |
114 | public void Dispose()
115 | {
116 | // Last tagger was disposed of. This is means there are no longer any open views on the buffer so we can safely shut down
117 | // lint checking for that buffer.
118 | _buffer.ChangedLowPriority -= this.OnBufferChange;
119 |
120 | _provider.RemoveLintChecker(this);
121 |
122 | IsDisposed = true;
123 |
124 | _buffer.Properties.RemoveProperty(typeof(LintChecker));
125 | }
126 |
127 | private void OnBufferChange(object sender, TextContentChangedEventArgs e)
128 | {
129 | _currentSnapshot = e.After;
130 |
131 | // Translate all of the old dirty spans to the new snapshot.
132 | NormalizedSnapshotSpanCollection newDirtySpans = _dirtySpans.CloneAndTrackTo(e.After, SpanTrackingMode.EdgeInclusive);
133 |
134 | // Dirty all the spans that changed.
135 | foreach (var change in e.Changes)
136 | {
137 | newDirtySpans = NormalizedSnapshotSpanCollection.Union(newDirtySpans, new NormalizedSnapshotSpanCollection(e.After, change.NewSpan));
138 | }
139 |
140 | // Translate all the linting errors to the new snapshot (and remove anything that is a dirty region since we will need to check that again).
141 | var oldErrors = this.Factory.CurrentSnapshot;
142 | var newErrors = new LintingErrorsSnapshot(oldErrors.Document, oldErrors.VersionNumber + 1);
143 |
144 | // Copy all of the old errors to the new errors unless the error was affected by the text change
145 | foreach (var error in oldErrors.Errors)
146 | {
147 | Debug.Assert(error.NextIndex == -1);
148 |
149 | var newError = LintError.CloneAndTranslateTo(error, e.After);
150 |
151 | if (newError != null)
152 | {
153 | Debug.Assert(newError.Span.Length == error.Span.Length);
154 |
155 | error.NextIndex = newErrors.Errors.Count;
156 | newErrors.Errors.Add(newError);
157 | }
158 | }
159 |
160 | this.UpdateLintingErrors(newErrors);
161 |
162 | _dirtySpans = newDirtySpans;
163 |
164 | // Start processing the dirty spans (which no-ops if we're already doing it).
165 | if (_dirtySpans.Count != 0)
166 | {
167 | this.RunLinter();
168 | }
169 | }
170 |
171 | private void RunLinter()
172 | {
173 | // We're assuming we will only be called from the UI thread so there should be no issues with race conditions.
174 | _cts?.Cancel();
175 | _cts = new CancellationTokenSource();
176 | this.Linting = Task.Run(() => DoUpdateAsync());
177 | }
178 |
179 | public async Task DoUpdateAsync()
180 | {
181 | if (IsDisposed)
182 | return;
183 |
184 | var buffer = _currentSnapshot;
185 | var path = Document.FilePath;
186 |
187 |
188 | // replace with user token
189 | var token = _cts.Token;
190 | var instance = await FsLintVsPackage.Instance.WithCancellation(token);
191 |
192 | if (token.IsCancellationRequested)
193 | return;
194 |
195 | // this acts as a throttle
196 | await Task.Delay(instance.Options.Throttle, token).ConfigureAwait(false);
197 |
198 | if (token.IsCancellationRequested)
199 | return;
200 |
201 | if (ProjectInfo == null)
202 | {
203 | await instance.JoinableTaskFactory.SwitchToMainThreadAsync();
204 | var solution = instance.Dte.Solution;
205 | var project = solution.FindProjectItem(path)?.ContainingProject;
206 |
207 | if (project == null)
208 | return;
209 |
210 | if (instance.SolutionService.GetProjectOfUniqueName(project.UniqueName, out var vsHierarchy) != VSConstants.S_OK)
211 | return;
212 |
213 | if (instance.SolutionService.GetGuidOfProject(vsHierarchy, out var guid) != VSConstants.S_OK)
214 | return;
215 |
216 | ProjectInfo = new LintProjectInfo(project, solution, guid, vsHierarchy);
217 | }
218 |
219 | if (Configuration == null)
220 | {
221 | this.Configuration =
222 | new[]
223 | {
224 | Document.FilePath,
225 | ProjectInfo.Project.FileName,
226 | ProjectInfo.Solution.FileName,
227 | Directory.GetParent(ProjectInfo.Solution.FileName).FullName
228 | }
229 | .Select(Path.GetDirectoryName)
230 | .Where(dir => !string.IsNullOrEmpty(dir))
231 | .Select(dir => Path.Combine(dir, "fsharplint.json"))
232 | .Where(File.Exists)
233 | .Select(Lint.ConfigurationParam.NewFromFile)
234 | .FirstOrDefault()
235 | ??
236 | Lint.ConfigurationParam.Default;
237 |
238 | }
239 |
240 | await Task.Yield();
241 |
242 | var lintOpts = new Lint.OptionalLintParameters(
243 | cancellationToken: token,
244 | configuration: this.Configuration,
245 | receivedWarning: null,
246 | reportLinterProgress: null);
247 |
248 | var source = _currentSnapshot.GetText();
249 | var sourceText = SourceText.ofString(source);
250 | var parse = instance.Options.TypeCheck ?
251 | TryParseAndCheckAsync(path, sourceText, token) :
252 | TryParseAsync(path, sourceText, token);
253 |
254 | var (parseResultsOpt, checkResults) = await parse.ConfigureAwait(false);
255 | if (parseResultsOpt == null || parseResultsOpt.Value.ParseHadErrors || token.IsCancellationRequested)
256 | return;
257 |
258 | var parseResults = parseResultsOpt.Value;
259 | var input = new Lint.ParsedFileInformation(ast: parseResults.ParseTree.Value, source, checkResults);
260 | var lintResult = Lint.lintParsedSource(lintOpts, input);
261 | if (!lintResult.TryGetSuccess(out var lintWarnings))
262 | return;
263 |
264 | var oldLintingErrors = this.Factory.CurrentSnapshot;
265 | var newLintErrors = new LintingErrorsSnapshot(Document, oldLintingErrors.VersionNumber + 1);
266 |
267 | foreach (var lint in lintWarnings)
268 | {
269 | var span = RangeToSpan(lint.Details.Range, buffer);
270 | newLintErrors.Errors.Add(new LintError(span, lint, ProjectInfo));
271 | }
272 |
273 | await instance.JoinableTaskFactory.SwitchToMainThreadAsync();
274 |
275 | if (token.IsCancellationRequested)
276 | return;
277 |
278 | UpdateLintingErrors(newLintErrors);
279 | }
280 |
281 | private async Task<(FSharpOption, FSharpOption)>
282 | TryParseAndCheckAsync(string path, ISourceText sourceText, CancellationToken token)
283 | {
284 | if (this.projectOptions == null)
285 | {
286 | var optionsAsync = _provider.CheckerInstance.GetProjectOptionsFromScript(
287 | filename: path,
288 | source: sourceText,
289 | assumeDotNetFramework: false,
290 | useSdkRefs: true,
291 | useFsiAuxLib: true,
292 | previewEnabled: true,
293 | otherFlags: new string[] { "--targetprofile:netstandard" },
294 | loadedTimeStamp: null,
295 | extraProjectInfo: null,
296 | optionsStamp: null,
297 | userOpName: null,
298 | sdkDirOverride: null
299 | );
300 |
301 | var (options, errors) = await FSharpAsync.StartAsTask(optionsAsync, null, token);
302 | if (!errors.IsEmpty)
303 | return (null, null);
304 |
305 | this.projectOptions = options;
306 | }
307 |
308 | var performParseAndCheck = _provider.CheckerInstance.ParseAndCheckFileInProject(
309 | filename: path,
310 | fileVersion: 1,
311 | sourceText: sourceText,
312 | options: projectOptions,
313 | userOpName: null
314 | );
315 |
316 | var (parseResults, checkAnswer) = await FSharpAsync.StartAsTask(performParseAndCheck, null, token);
317 | if (checkAnswer is FSharpCheckFileAnswer.Succeeded succeeded)
318 | return (parseResults, succeeded.Item);
319 |
320 | return (parseResults, null);
321 | }
322 |
323 | private async Task<(FSharpOption, FSharpOption)>
324 | TryParseAsync(string path, ISourceText sourceText, CancellationToken token)
325 | {
326 | if(parseOpts == null)
327 | {
328 | var defaults = FSharpParsingOptions.Default;
329 | this.parseOpts = new FSharpParsingOptions(
330 | sourceFiles: new string[] { path },
331 | conditionalCompilationDefines: defaults.ConditionalCompilationDefines,
332 | errorSeverityOptions: defaults.ErrorSeverityOptions,
333 | isInteractive: defaults.IsInteractive,
334 | lightSyntax: defaults.LightSyntax,
335 | compilingFsLib: defaults.CompilingFsLib,
336 | isExe: defaults.IsExe
337 | );
338 | }
339 |
340 | var parseAsync = _provider.CheckerInstance.ParseFile(path, sourceText, parseOpts, "FsLint");
341 | var parseResults = await FSharpAsync.StartAsTask(parseAsync, null, token).ConfigureAwait(false);
342 | return (parseResults, null);
343 | }
344 |
345 | public static SnapshotSpan RangeToSpan(Range fsrange, ITextSnapshot textSnapshot)
346 | {
347 | var from = fsrange.StartLine - 1;
348 | ITextSnapshotLine anchor = textSnapshot.GetLineFromLineNumber(from);
349 | var start = anchor.Start.Position + fsrange.StartColumn;
350 | var to = fsrange.EndLine - 1;
351 | var end = textSnapshot.GetLineFromLineNumber(to).Start.Position + fsrange.EndColumn;
352 | return new SnapshotSpan(textSnapshot, new Span(start, end - start));
353 | }
354 |
355 | public static ITrackingSpan RangeToTrackingSpan(Range fsrange, ITextSnapshot textSnapshot)
356 | {
357 | var from = fsrange.StartLine - 1;
358 | ITextSnapshotLine anchor = textSnapshot.GetLineFromLineNumber(from);
359 | var start = anchor.Start.Position + fsrange.StartColumn;
360 | var to = fsrange.EndLine - 1;
361 | var end = textSnapshot.GetLineFromLineNumber(to).Start.Position + fsrange.EndColumn;
362 | return textSnapshot.CreateTrackingSpan(start, end - start, SpanTrackingMode.EdgeExclusive);
363 | }
364 |
365 | private void UpdateLintingErrors(LintingErrorsSnapshot lintSnapshot)
366 | {
367 | // Tell our factory to snap to a new snapshot.
368 | this.Factory.UpdateErrors(lintSnapshot);
369 |
370 | // Tell the provider to mark all the sinks dirty (so, as a side-effect, they will start an update pass that will get the new snapshot
371 | // from the factory).
372 | _provider.NotifyAllSinks();
373 |
374 | foreach (var tagger in _activeTaggers)
375 | {
376 | tagger.UpdateErrors(_currentSnapshot, lintSnapshot);
377 | }
378 |
379 | this.LastErrorsSnapshot = lintSnapshot;
380 | Updated?.Invoke(this, EventArgs.Empty);
381 | }
382 |
383 | }
384 | }
385 |
--------------------------------------------------------------------------------
/src/LintCheckerProvider.cs:
--------------------------------------------------------------------------------
1 | using FSharp.Compiler.SourceCodeServices;
2 | using Microsoft.VisualStudio.Shell.TableControl;
3 | using Microsoft.VisualStudio.Shell.TableManager;
4 | using Microsoft.VisualStudio.Text;
5 | using Microsoft.VisualStudio.Text.Editor;
6 | using Microsoft.VisualStudio.Text.Tagging;
7 | using Microsoft.VisualStudio.Utilities;
8 | using System;
9 | using System.Collections.Generic;
10 | using System.ComponentModel.Composition;
11 |
12 | namespace FSharpLintVs
13 | {
14 | ///
15 | /// Factory for the and .
16 | /// There will be only one instance of this class created.
17 | ///
18 | [Export(typeof(IViewTaggerProvider))]
19 | [TagType(typeof(IErrorTag))]
20 | [ContentType(ContentTypeNames.FSharpContentType)]
21 | [TextViewRole(PredefinedTextViewRoles.Document)]
22 | [TextViewRole(PredefinedTextViewRoles.Analyzable)]
23 | public sealed class LintCheckerProvider : IViewTaggerProvider, ITableDataSource
24 | {
25 | public readonly ITableManager ErrorTableManager;
26 | public readonly ITextDocumentFactoryService TextDocumentFactoryService;
27 |
28 | public const string LintCheckerDataSource = "LintChecker";
29 |
30 | private readonly List _managers = new List(); // Also used for locks
31 | private readonly List _lintCheckers = new List();
32 |
33 | [ImportingConstructor]
34 | public LintCheckerProvider
35 | (
36 | [Import] ITableManagerProvider provider,
37 | [Import] ITextDocumentFactoryService textDocumentFactoryService
38 | )
39 | {
40 | this.TextDocumentFactoryService = textDocumentFactoryService;
41 | this.ErrorTableManager = provider.GetTableManager(StandardTables.ErrorsTable);
42 | this.ErrorTableManager.AddSource(this,
43 | StandardTableColumnDefinitions.DetailsExpander,
44 | StandardTableColumnDefinitions.ErrorSeverity,
45 | StandardTableColumnDefinitions.ErrorCode,
46 | StandardTableColumnDefinitions.ErrorSource,
47 | StandardTableColumnDefinitions.BuildTool,
48 | StandardTableColumnDefinitions.ErrorSource,
49 | StandardTableColumnDefinitions.ErrorCategory,
50 | StandardTableColumnDefinitions.Text,
51 | StandardTableColumnDefinitions.DocumentName,
52 | StandardTableColumnDefinitions.Line,
53 | StandardTableColumnDefinitions.Column,
54 | StandardTableColumnDefinitions.ProjectName
55 | );
56 | }
57 |
58 | ///
59 | /// Create a tagger that does lint checking on the view/buffer combination.
60 | ///
61 | public ITagger CreateTagger(ITextView textView, ITextBuffer buffer) where T : ITag
62 | {
63 | ITagger tagger = null;
64 |
65 | // Only attempt to lint check on the view's edit buffer (and multiple views could have that buffer open simultaneously so
66 | // only create one instance of the lint checker.
67 | if ((buffer == textView.TextBuffer) && (typeof(T) == typeof(IErrorTag)))
68 | {
69 | var lintChecker = buffer.Properties.GetOrCreateSingletonProperty(typeof(LintChecker), () => new LintChecker(this, textView, buffer));
70 |
71 | // This is a thin wrapper around the LintChecker that can be disposed of without shutting down the LintChecker
72 | // (unless it was the last tagger on the lint checker).
73 | tagger = new LintTagger(lintChecker) as ITagger;
74 | }
75 |
76 | return tagger;
77 | }
78 |
79 | #region ITableDataSource members
80 |
81 | // This string should, in general, be localized since it is what would be displayed in any UI that lets the end user pick
82 | // which ITableDataSources should be subscribed to by an instance of the table control. It really isn't needed for the error
83 | // list however because it autosubscribes to all the ITableDataSources.
84 | public string DisplayName => "F# Lint";
85 |
86 | public string Identifier => LintCheckerDataSource;
87 |
88 | public string SourceTypeIdentifier => StandardTableDataSources.ErrorTableDataSource;
89 |
90 | // This is the observer pattern
91 | public IDisposable Subscribe(ITableDataSink sink)
92 | {
93 | // This method is called to each consumer interested in errors. In general, there will be only a single consumer (the error list tool window)
94 | // but it is always possible for 3rd parties to write code that will want to subscribe.
95 | return new SubscriptionManager(this, sink);
96 | }
97 | #endregion
98 |
99 | #region Checker
100 |
101 | private readonly Lazy _checker = new Lazy(() =>
102 | FSharpChecker.Create(null, null, null, null, null, null, null, null, null)
103 | );
104 |
105 | public FSharpChecker CheckerInstance => _checker.Value;
106 |
107 | #endregion
108 |
109 | public void AddSinkManager(SubscriptionManager manager)
110 | {
111 | // This call can, in theory, happen from any thread so be appropriately thread safe.
112 | // In practice, it will probably be called only once from the UI thread (by the error list tool window).
113 | lock (_managers)
114 | {
115 | _managers.Add(manager);
116 |
117 | // Add the pre-existing lint checkers to the manager.
118 | foreach (var checker in _lintCheckers)
119 | {
120 | manager.Add(checker);
121 | }
122 | }
123 | }
124 |
125 | public void RemoveSinkManager(SubscriptionManager manager)
126 | {
127 | // This call can, in theory, happen from any thread so be appropriately thread safe.
128 | // In practice, it will probably be called only once from the UI thread (by the error list tool window).
129 | lock (_managers)
130 | {
131 | _managers.Remove(manager);
132 | }
133 | }
134 |
135 | public void AddLintChecker(LintChecker lintChecker)
136 | {
137 | // This call will always happen on the UI thread (it is a side-effect of adding or removing the 1st/last tagger).
138 | lock (_managers)
139 | {
140 | _lintCheckers.Add(lintChecker);
141 |
142 | // Tell the preexisting managers about the new lint checker
143 | foreach (var manager in _managers)
144 | {
145 | manager.Add(lintChecker);
146 | }
147 | }
148 | }
149 |
150 | public void RemoveLintChecker(LintChecker lintChecker)
151 | {
152 | // This call will always happen on the UI thread (it is a side-effect of adding or removing the 1st/last tagger).
153 | lock (_managers)
154 | {
155 | _lintCheckers.Remove(lintChecker);
156 |
157 | foreach (var manager in _managers)
158 | {
159 | manager.Remove(lintChecker);
160 | }
161 | }
162 | }
163 |
164 | public void NotifyAllSinks()
165 | {
166 | lock (_managers)
167 | {
168 | foreach (var manager in _managers)
169 | {
170 | manager.Notify();
171 | }
172 | }
173 | }
174 |
175 | }
176 | }
177 |
--------------------------------------------------------------------------------
/src/LintError.cs:
--------------------------------------------------------------------------------
1 | using FSharpLint.Framework;
2 | using Microsoft.VisualStudio.Text;
3 |
4 | namespace FSharpLintVs
5 | {
6 | public class LintError
7 | {
8 | public readonly SnapshotSpan Span;
9 | public readonly LintProjectInfo Project;
10 | private readonly Suggestion.LintWarning LintWarning;
11 |
12 | public int NextIndex = -1;
13 |
14 | public string Tooltip => $"{LintWarning.RuleIdentifier}: {LintWarning.Details.Message}";
15 |
16 | public string Identifier => LintWarning.RuleIdentifier;
17 |
18 | public string Name => LintWarning.RuleName;
19 |
20 | public string Message => LintWarning.Details.Message;
21 |
22 | public string HelpUrl => $"https://fsprojects.github.io/FSharpLint/how-tos/rules/{Identifier}.html";
23 |
24 | public bool HasSuggestedFix => this.LintWarning.Details.SuggestedFix?.Value?.Value != null;
25 |
26 | public Suggestion.SuggestedFix GetSuggestedFix() => LintWarning.Details.SuggestedFix.Value.Value.Value;
27 |
28 | public int Line => LintWarning.Details.Range.StartLine - 1;
29 |
30 | public int Column => LintWarning.Details.Range.StartColumn;
31 |
32 | public string ErrorText => LintWarning.ErrorText;
33 |
34 | public string ReplacementText
35 | {
36 | get
37 | {
38 | var fix = GetSuggestedFix();
39 | if (fix == null)
40 | return "";
41 |
42 | var startColumn = fix.FromRange.StartColumn;
43 | return ErrorText.Remove(startColumn, fix.FromRange.EndColumn - startColumn).Insert(startColumn, fix.ToText);
44 | }
45 | }
46 |
47 | public LintError(SnapshotSpan span, Suggestion.LintWarning lintWarning, LintProjectInfo project)
48 | {
49 | this.Span = span;
50 | this.LintWarning = lintWarning;
51 | this.Project = project;
52 | }
53 |
54 | public static LintError Clone(LintError error)
55 | {
56 | return new LintError(error.Span, error.LintWarning, error.Project);
57 | }
58 |
59 | public static LintError CloneAndTranslateTo(LintError error, ITextSnapshot newSnapshot)
60 | {
61 | var newSpan = error.Span.TranslateTo(newSnapshot, SpanTrackingMode.EdgeExclusive);
62 |
63 | // We want to only translate the error if the length of the error span did not change (if it did change, it would imply that
64 | // there was some text edit inside the error and, therefore, that the error is no longer valid).
65 | return (newSpan.Length == error.Span.Length)
66 | ? new LintError(newSpan, error.LintWarning, error.Project)
67 | : null;
68 | }
69 |
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/src/LintProjectInfo.cs:
--------------------------------------------------------------------------------
1 | using EnvDTE;
2 | using Microsoft.VisualStudio.Shell.Interop;
3 | using System;
4 |
5 | namespace FSharpLintVs
6 | {
7 | public class LintProjectInfo
8 | {
9 | public Project Project { get; }
10 |
11 | public string ProjectName { get; }
12 |
13 | // Performance will be improved if you "prebox" your System.Guid by,
14 | // in your Microsoft.VisualStudio.Shell.TableManager.ITableEntry
15 | // or Microsoft.VisualStudio.Shell.TableManager.ITableEntriesSnapshot, having a
16 | // member variable:
17 | // private object boxedProjectGuid = projectGuid;
18 | // and returning boxedProjectGuid instead of projectGuid.
19 | public object ProjectGuid { get; }
20 |
21 | public IVsHierarchy Hierarchy { get; }
22 |
23 | public EnvDTE.Solution Solution { get; }
24 |
25 | public LintProjectInfo(EnvDTE.Project project, EnvDTE.Solution solution, Guid projectGuid, IVsHierarchy hierarchy)
26 | {
27 | Microsoft.VisualStudio.Shell.ThreadHelper.ThrowIfNotOnUIThread();
28 |
29 | Project = project;
30 | ProjectName = project.Name;
31 | ProjectGuid = projectGuid;
32 | Hierarchy = hierarchy;
33 | Solution = solution;
34 |
35 | }
36 |
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/LintTagger.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.VisualStudio.Text;
2 | using Microsoft.VisualStudio.Text.Adornments;
3 | using Microsoft.VisualStudio.Text.Tagging;
4 | using System;
5 | using System.Collections.Generic;
6 |
7 | namespace FSharpLintVs
8 | {
9 | public class LintTagger : ITagger, IDisposable
10 | {
11 | private readonly LintChecker _lintChecker;
12 | private LintingErrorsSnapshot _snapshot;
13 |
14 | public LintTagger(LintChecker lintChecker)
15 | {
16 | _lintChecker = lintChecker;
17 | _snapshot = lintChecker.LastErrorsSnapshot;
18 |
19 | lintChecker.AddTagger(this);
20 | }
21 |
22 | public event EventHandler TagsChanged;
23 |
24 | public void UpdateErrors(ITextSnapshot currentSnapshot, LintingErrorsSnapshot lintingErrors)
25 | {
26 | var oldLintingErrors = _snapshot;
27 | _snapshot = lintingErrors;
28 |
29 |
30 | // Raise a single tags changed event over the span that could have been affected by the change in the errors.
31 | var start = int.MaxValue;
32 | var end = int.MinValue;
33 |
34 | if (oldLintingErrors?.Errors.Count > 0)
35 | {
36 | start = oldLintingErrors.Errors[0].Span.Start.TranslateTo(currentSnapshot, PointTrackingMode.Negative);
37 | end = oldLintingErrors.Errors[oldLintingErrors.Errors.Count - 1].Span.End.TranslateTo(currentSnapshot, PointTrackingMode.Positive);
38 | }
39 |
40 | if (lintingErrors.Count > 0)
41 | {
42 | start = Math.Min(start, lintingErrors.Errors[0].Span.Start.Position);
43 | end = Math.Max(end, lintingErrors.Errors[lintingErrors.Errors.Count - 1].Span.End.Position);
44 | }
45 |
46 | if (start < end)
47 | {
48 | TagsChanged?.Invoke(this, new SnapshotSpanEventArgs(new SnapshotSpan(currentSnapshot, Span.FromBounds(start, end))));
49 | }
50 | }
51 |
52 | public IEnumerable> GetTags(NormalizedSnapshotSpanCollection spans)
53 | {
54 | if (_snapshot != null)
55 | {
56 | foreach (var error in _snapshot.Errors)
57 | {
58 | if (spans.IntersectsWith(error.Span))
59 | {
60 | yield return new TagSpan(error.Span, new ErrorTag(PredefinedErrorTypeNames.Warning, error.Tooltip) { });
61 | }
62 | }
63 | }
64 | }
65 |
66 | public void Dispose()
67 | {
68 | // Called when the tagger is no longer needed (generally when the ITextView is closed).
69 | _lintChecker.RemoveTagger(this);
70 | }
71 |
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/src/Options/FsLintOptionsPage.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.ComponentModel;
3 | using Microsoft.VisualStudio.Shell;
4 | using System.Runtime.InteropServices;
5 |
6 | namespace FSharpLintVs
7 | {
8 | [Guid(GuidString)]
9 | public class FsLintOptionsPage : DialogPage
10 | {
11 | public const string GuidString = "74927147-72e8-4b47-a70d-5568807d6879";
12 |
13 | [Category("Performance")]
14 | [DisplayName("Throttle Interval (ms)")]
15 | [Description("Wait for this much time after an edit before running the linter. " +
16 | "Lower values will make it more responsive, but increase CPU usage. " +
17 | "High values might make the linter seem sluggish.")]
18 | public int Throttle { get; set; } = 200;
19 |
20 | [Category("Checking")]
21 | [DisplayName("Type-Check Files")]
22 | [Description("Type check the file in the current project. " +
23 | "Regular linting requires only the AST. " +
24 | "Turning this on enables a few type-checked rules, but makes the linter slower.")]
25 | public bool TypeCheck { get; set; } = false;
26 |
27 | public event Action Applied;
28 | protected override void OnApply(PageApplyEventArgs e)
29 | {
30 | if(e.ApplyBehavior == ApplyKind.Apply)
31 | {
32 | Applied?.Invoke();
33 | }
34 |
35 | base.OnApply(e);
36 | }
37 |
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/Properties/AssemblyInfo.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using System.Runtime.CompilerServices;
3 | using System.Runtime.InteropServices;
4 |
5 | // General Information about an assembly is controlled through the following
6 | // set of attributes. Change these attribute values to modify the information
7 | // associated with an assembly.
8 | [assembly: AssemblyTitle("FsLintVs")]
9 | [assembly: AssemblyDescription("F# Linter Extension for Visual Studio")]
10 | [assembly: AssemblyConfiguration("")]
11 | [assembly: AssemblyCompany("asti")]
12 | [assembly: AssemblyProduct("FsLintVs")]
13 | [assembly: AssemblyCopyright("")]
14 | [assembly: AssemblyTrademark("")]
15 | [assembly: AssemblyCulture("")]
16 |
17 | // Setting ComVisible to false makes the types in this assembly not visible
18 | // to COM components. If you need to access a type in this assembly from
19 | // COM, set the ComVisible attribute to true on that type.
20 | [assembly: ComVisible(false)]
21 |
22 | // Version information for an assembly consists of the following four values:
23 | //
24 | // Major Version
25 | // Minor Version
26 | // Build Number
27 | // Revision
28 | //
29 | // You can specify all the values or you can default the Build and Revision Numbers
30 | // by using the '*' as shown below:
31 | // [assembly: AssemblyVersion("1.0.*")]
32 | [assembly: AssemblyVersion("0.2.0.0")]
33 | [assembly: AssemblyFileVersion("0.2.0.0")]
34 |
--------------------------------------------------------------------------------
/src/Resources/License.txt:
--------------------------------------------------------------------------------
1 | Copyright 2020 Asti
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4 |
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 |
7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8 |
9 |
--------------------------------------------------------------------------------
/src/Resources/ReleaseNotes.html:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fsprojects/fsharp-linting-for-vs/2bb72bb1724056b068b0cecbbed579f44827f6f1/src/Resources/ReleaseNotes.html
--------------------------------------------------------------------------------
/src/Resources/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fsprojects/fsharp-linting-for-vs/2bb72bb1724056b068b0cecbbed579f44827f6f1/src/Resources/logo.png
--------------------------------------------------------------------------------
/src/SubscriptionManager.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.VisualStudio.Shell.TableManager;
2 | using System;
3 |
4 | namespace FSharpLintVs
5 | {
6 | ///
7 | /// Every consumer of data from an provides an to record the changes. We give the consumer
8 | /// an IDisposable (this object) that they hang on to as long as they are interested in our data (and they Dispose() of it when they are done).
9 | ///
10 | public class SubscriptionManager : IDisposable
11 | {
12 | private readonly LintCheckerProvider _lintCheckerProvider;
13 | private readonly ITableDataSink _sink;
14 |
15 | public SubscriptionManager(LintCheckerProvider lintCheckerProvider, ITableDataSink sink)
16 | {
17 | _lintCheckerProvider = lintCheckerProvider;
18 | _sink = sink;
19 |
20 | lintCheckerProvider.AddSinkManager(this);
21 | }
22 |
23 | public void Add(LintChecker lintChecker)
24 | {
25 | _sink.AddFactory(lintChecker.Factory);
26 | }
27 |
28 | public void Remove(LintChecker lintChecker)
29 | {
30 | _sink.RemoveFactory(lintChecker.Factory);
31 | }
32 |
33 | public void Notify()
34 | {
35 | _sink.FactorySnapshotChanged(null);
36 | }
37 |
38 | public void Dispose()
39 | {
40 | // Called when the person who subscribed to the data source disposes of the cookie
41 | // (== this object) they were given.
42 | _lintCheckerProvider.RemoveSinkManager(this);
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/SuggestionPreview.xaml:
--------------------------------------------------------------------------------
1 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
24 |
25 | . . .
26 |
27 | -
28 |
29 |
30 |
31 |
32 |
33 |
34 | +
35 |
36 |
37 |
38 |
39 |
40 | . . .
41 |
42 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/src/SuggestionPreview.xaml.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 | using System.Windows;
7 | using System.Windows.Controls;
8 | using System.Windows.Data;
9 | using System.Windows.Documents;
10 | using System.Windows.Input;
11 | using System.Windows.Media;
12 | using System.Windows.Media.Imaging;
13 | using System.Windows.Navigation;
14 | using System.Windows.Shapes;
15 |
16 | namespace FSharpLintVs
17 | {
18 | ///
19 | /// Interaction logic for SuggestionPreview.xaml
20 | ///
21 | public partial class SuggestionPreview : UserControl
22 | {
23 | protected SuggestionPreview()
24 | {
25 | InitializeComponent();
26 | }
27 |
28 | public SuggestionPreview(LintError lintError) : this()
29 | {
30 | this.DataContext = lintError;
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/Suggestions/LintActionsSource.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.VisualStudio.Language.Intellisense;
2 | using Microsoft.VisualStudio.Text;
3 | using Microsoft.VisualStudio.Text.Editor;
4 | using Microsoft.VisualStudio.Text.Operations;
5 | using Microsoft.VisualStudio.Threading;
6 | using System;
7 | using System.Collections.Generic;
8 | using System.Linq;
9 | using System.Net.Http.Headers;
10 | using System.Threading;
11 | using System.Threading.Tasks;
12 |
13 | namespace FSharpLintVs
14 | {
15 | public class LintActionsSource : ISuggestedActionsSource
16 | {
17 | private readonly ITextBuffer _textBuffer;
18 | private LintChecker _lintChecker;
19 |
20 | public LintActionsSource(ITextBuffer textBuffer)
21 | {
22 | _textBuffer = textBuffer;
23 | }
24 |
25 | #pragma warning disable 0067
26 | public event EventHandler SuggestedActionsChanged;
27 | protected void OnSuggestedActionsChanged(object sender, EventArgs e)
28 | {
29 | SuggestedActionsChanged?.Invoke(sender, e);
30 | }
31 | #pragma warning restore 0067
32 |
33 | public void Dispose()
34 | {
35 | if (_lintChecker != null)
36 | _lintChecker.Updated -= OnSuggestedActionsChanged;
37 | }
38 |
39 | public IEnumerable GetSuggestedActions(ISuggestedActionCategorySet requestedActionCategories, SnapshotSpan range, CancellationToken cancellationToken)
40 | {
41 | yield return new SuggestedActionSet(
42 | categoryName: PredefinedSuggestedActionCategoryNames.CodeFix,
43 | actions: GetSuggestedActions(range),
44 | title: "FsLint",
45 | priority: SuggestedActionSetPriority.None,
46 | applicableToSpan: null
47 | );
48 | }
49 |
50 | public IEnumerable GetSuggestedActions(SnapshotSpan range)
51 | {
52 | if (!TryGetLintChecker(out var lintChecker))
53 | yield break;
54 |
55 | if (!lintChecker.HasSnapshot)
56 | yield break;
57 |
58 | foreach (var error in lintChecker.LastErrorsSnapshot.Errors)
59 | {
60 | if (range.IntersectsWith(error.Span))
61 | {
62 | if (error.HasSuggestedFix)
63 | yield return new LintFixAction(error);
64 |
65 | yield return new LintSuppressAction(error);
66 | }
67 | }
68 | }
69 |
70 | public Task HasSuggestedActionsAsync(ISuggestedActionCategorySet requestedActionCategories, SnapshotSpan range, CancellationToken cancellationToken)
71 | {
72 | return Task.Run(async () =>
73 | {
74 | if (!TryGetLintChecker(out var lintChecker))
75 | return false;
76 |
77 | // wait for linting to complete
78 | await lintChecker.Linting.WithCancellation(cancellationToken);
79 |
80 | if (!lintChecker.HasSnapshot)
81 | return false;
82 |
83 | // we can't actually traverse the to see if the suggested action is a Some (fix) or None
84 | // because we'd have to evaluate the lazy
85 | return lintChecker.LastErrorsSnapshot.Count > 0;
86 |
87 | }, cancellationToken);
88 | }
89 |
90 | private bool TryGetLintChecker(out LintChecker checker)
91 | {
92 | // return cached value
93 | if (_lintChecker != null)
94 | {
95 | checker = _lintChecker;
96 | return true;
97 | }
98 |
99 | if (!_textBuffer.Properties.TryGetProperty(typeof(LintChecker), out checker))
100 | return false;
101 |
102 | if (checker.IsDisposed || checker.RefCount == 0 || checker.Linting == null)
103 | return false;
104 |
105 | // cache value
106 | _lintChecker = checker;
107 | _lintChecker.Updated += OnSuggestedActionsChanged;
108 | return true;
109 | }
110 |
111 |
112 | public bool TryGetTelemetryId(out Guid telemetryId)
113 | {
114 | telemetryId = Guid.Empty;
115 | return false;
116 | }
117 |
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/src/Suggestions/LintFixAction.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Threading;
4 | using System.Threading.Tasks;
5 | using System.Windows;
6 | using System.Windows.Controls;
7 | using System.Windows.Documents;
8 | using System.Windows.Media;
9 | using FSharpLint.Framework;
10 | using Microsoft.VisualStudio.Imaging;
11 | using Microsoft.VisualStudio.Imaging.Interop;
12 | using Microsoft.VisualStudio.Language.Intellisense;
13 | using Microsoft.VisualStudio.Text;
14 |
15 | namespace FSharpLintVs
16 | {
17 | public class LintFixAction : ISuggestedAction
18 | {
19 | private readonly LintError _lintError;
20 | private readonly Suggestion.SuggestedFix _fix;
21 |
22 | public LintFixAction(LintError lintError)
23 | {
24 | this._lintError = lintError;
25 | this._fix = _lintError.GetSuggestedFix();
26 | }
27 |
28 | public string DisplayText => $"Replace with '{_fix.ToText}'";
29 |
30 | public string IconAutomationText => null;
31 |
32 | ImageMoniker ISuggestedAction.IconMoniker => KnownMonikers.CodeWarningRule;
33 |
34 | public string InputGestureText => null;
35 |
36 | public bool HasActionSets => false;
37 |
38 | #pragma warning disable RCS1210
39 | public Task> GetActionSetsAsync(CancellationToken cancellationToken) => default;
40 | #pragma warning restore RCS1210
41 |
42 | public bool HasPreview => true;
43 |
44 | public Task