├── .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 | ![fslint](https://user-images.githubusercontent.com/2375486/90334848-1f62ca80-dfee-11ea-932d-af0d330e4e8c.gif) 28 | 29 | ## Options 30 | 31 | Linter options can be found in `F# Tools > Linting` 32 | 33 | ![FsLintOptions](https://user-images.githubusercontent.com/2375486/96405667-6beba180-11fb-11eb-821d-aff9e858a7d8.jpg) 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 GetPreviewAsync(CancellationToken cancellationToken) 45 | { 46 | var textBlock = new SuggestionPreview(this._lintError) { MaxWidth = 400, MinHeight = 100 }; 47 | return Task.FromResult(textBlock); 48 | } 49 | 50 | public void Invoke(CancellationToken cancellationToken) 51 | { 52 | if (cancellationToken.IsCancellationRequested) 53 | return; 54 | 55 | var span = LintChecker.RangeToSpan(_fix.FromRange, _lintError.Span.Snapshot); 56 | span.Snapshot.TextBuffer.Replace(span, _fix.ToText); 57 | } 58 | 59 | public bool TryGetTelemetryId(out Guid telemetryId) 60 | { 61 | telemetryId = Guid.Empty; 62 | return false; 63 | } 64 | 65 | public void Dispose() 66 | { 67 | //nothing to do 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Suggestions/LintSuggestionProvider.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.VisualStudio.Language.Intellisense; 2 | using Microsoft.VisualStudio.Text; 3 | using Microsoft.VisualStudio.Text.Editor; 4 | using Microsoft.VisualStudio.Utilities; 5 | using System.ComponentModel.Composition; 6 | 7 | namespace FSharpLintVs 8 | { 9 | [Export(typeof(ISuggestedActionsSourceProvider))] 10 | [ContentType(ContentTypeNames.FSharpContentType)] 11 | [Name("F# Lint Suggested Actions")] 12 | public class LintSuggestionProvider : ISuggestedActionsSourceProvider 13 | { 14 | 15 | public ISuggestedActionsSource CreateSuggestedActionsSource(ITextView textView, ITextBuffer textBuffer) 16 | { 17 | return new LintActionsSource(textBuffer); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Suggestions/LintSuppressAction.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.VisualStudio.Imaging; 2 | using Microsoft.VisualStudio.Imaging.Interop; 3 | using Microsoft.VisualStudio.Language.Intellisense; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | using System.Windows; 10 | 11 | namespace FSharpLintVs 12 | { 13 | public class LintSuppressAction : ISuggestedAction 14 | { 15 | private readonly LintError error; 16 | 17 | public LintSuppressAction(LintError error) 18 | { 19 | this.error = error; 20 | } 21 | 22 | public string DisplayText => $"Suppress {error.Identifier}"; 23 | 24 | public ImageMoniker IconMoniker => KnownMonikers.CodeSuppressedRule; 25 | 26 | public string IconAutomationText => default; 27 | 28 | public string InputGestureText => default; 29 | 30 | public bool HasPreview => false; 31 | 32 | public bool HasActionSets => true; 33 | 34 | public Task> GetActionSetsAsync(CancellationToken cancellationToken) 35 | { 36 | return Task.FromResult(new SuggestedActionSet[] 37 | { 38 | new SuggestedActionSet( 39 | categoryName: PredefinedSuggestedActionCategoryNames.CodeFix, 40 | actions: GetSuggestedActions(), 41 | title: "FsLint", 42 | priority: SuggestedActionSetPriority.None, 43 | applicableToSpan: null 44 | ) 45 | }.AsEnumerable()); 46 | } 47 | 48 | public IEnumerable GetSuggestedActions() 49 | { 50 | yield return new LintSuppressBy(error, LintSuppressBy.Method.Above); 51 | yield return new LintSuppressBy(error, LintSuppressBy.Method.Inline); 52 | yield return new LintSuppressBy(error, LintSuppressBy.Method.Section); 53 | } 54 | 55 | public Task GetPreviewAsync(CancellationToken cancellationToken) => throw new NotImplementedException(); 56 | 57 | public void Invoke(CancellationToken cancellationToken) 58 | { 59 | GetSuggestedActions().First().Invoke(cancellationToken); 60 | } 61 | 62 | public bool TryGetTelemetryId(out Guid telemetryId) 63 | { 64 | telemetryId = default; 65 | return false; 66 | } 67 | 68 | public void Dispose() 69 | { 70 | // nothing 71 | } 72 | } 73 | 74 | 75 | } -------------------------------------------------------------------------------- /src/Suggestions/LintSuppressBy.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.VisualStudio.Imaging; 2 | using Microsoft.VisualStudio.Imaging.Interop; 3 | using Microsoft.VisualStudio.Language.Intellisense; 4 | using Microsoft.VisualStudio.Text; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using System.Threading; 9 | using System.Threading.Tasks; 10 | 11 | namespace FSharpLintVs 12 | { 13 | public class LintSuppressBy : ISuggestedAction 14 | { 15 | public enum Method 16 | { 17 | Inline, 18 | Above, 19 | Section, 20 | Global 21 | } 22 | 23 | private readonly LintError _error; 24 | private readonly Method _suppressionMethod; 25 | 26 | public LintSuppressBy(LintError error, Method suppressionMethod) 27 | { 28 | this._error = error; 29 | this._suppressionMethod = suppressionMethod; 30 | } 31 | 32 | public static string ToText(Method suppressionMethod) => 33 | suppressionMethod switch 34 | { 35 | Method.Inline => "inline", 36 | Method.Above => "above", 37 | Method.Section => "section", 38 | _ => "...", 39 | }; 40 | 41 | public string DisplayText => $"Suppress {ToText(_suppressionMethod)}"; 42 | 43 | public ImageMoniker IconMoniker => _suppressionMethod switch 44 | { 45 | Method.Above => KnownMonikers.GlyphUp, 46 | Method.Section => KnownMonikers.Inline, 47 | Method.Inline => KnownMonikers.GoToCurrentLine, 48 | _ => default 49 | }; 50 | 51 | public string IconAutomationText => default; 52 | 53 | public string InputGestureText => default; 54 | 55 | public bool HasActionSets => false; 56 | 57 | public Task> GetActionSetsAsync(CancellationToken cancellationToken) 58 | { 59 | // This method should not be called by VS 60 | throw new NotSupportedException(); 61 | } 62 | 63 | public bool HasPreview => false; 64 | 65 | public Task GetPreviewAsync(CancellationToken cancellationToken) => throw new NotImplementedException(); 66 | 67 | public void Invoke(CancellationToken cancellationToken) 68 | { 69 | if (cancellationToken.IsCancellationRequested) 70 | return; 71 | 72 | var snapshot = _error.Span.Snapshot; 73 | 74 | switch (_suppressionMethod) 75 | { 76 | case Method.Inline: 77 | { 78 | var line = _error.Span.Start.GetContainingLine(); 79 | if (line != null) 80 | snapshot.TextBuffer.Insert(line.End.Position, $" // fsharplint:disable-line {_error.Name}"); 81 | break; 82 | } 83 | case Method.Above: 84 | { 85 | var line = snapshot.GetLineFromLineNumber(_error.Line - 1); 86 | var indent = snapshot.GetLineFromLineNumber(_error.Line).GetText().TakeWhile(Char.IsWhiteSpace).Count(); 87 | if (line != null) 88 | snapshot.TextBuffer.Insert(line.End.Position, $"{Environment.NewLine}{new String(' ', indent)}// fsharplint:disable-next-line {_error.Name}"); 89 | break; 90 | } 91 | case Method.Section: 92 | { 93 | var containingLine = FindSection().FirstOrDefault(); 94 | if (containingLine == null) 95 | { 96 | snapshot = _error.Span.Snapshot.TextBuffer.Insert(0, $"// fsharplint:disable{Environment.NewLine}"); 97 | containingLine = snapshot.GetLineFromLineNumber(0); 98 | } 99 | 100 | snapshot.TextBuffer.Insert(containingLine.End, $" {_error.Name}"); 101 | break; 102 | } 103 | default: 104 | break; 105 | } 106 | } 107 | 108 | public IEnumerable FindSection() 109 | { 110 | var snapshot = _error.Span.Snapshot; 111 | var cursor = _error.Line + 1; 112 | 113 | while (--cursor >= 0) 114 | { 115 | var line = snapshot.GetLineFromLineNumber(cursor); 116 | if (line.GetText().StartsWith("// fsharplint:disable ")) 117 | yield return line; 118 | } 119 | } 120 | 121 | public bool TryGetTelemetryId(out Guid telemetryId) 122 | { 123 | telemetryId = default; 124 | return false; 125 | } 126 | 127 | public void Dispose() 128 | { 129 | // nothing 130 | } 131 | } 132 | 133 | 134 | } -------------------------------------------------------------------------------- /src/Table/LintTableSnapshotFactory.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.VisualStudio.Shell.TableManager; 2 | 3 | namespace FSharpLintVs 4 | { 5 | public class LintTableSnapshotFactory : TableEntriesSnapshotFactoryBase 6 | { 7 | 8 | public LintingErrorsSnapshot CurrentSnapshot { get; private set; } 9 | 10 | public LintTableSnapshotFactory(LintingErrorsSnapshot lintErrors) 11 | { 12 | this.CurrentSnapshot = lintErrors; 13 | } 14 | 15 | public void UpdateErrors(LintingErrorsSnapshot lintingErrors) 16 | { 17 | this.CurrentSnapshot.NextSnapshot = lintingErrors; 18 | this.CurrentSnapshot = lintingErrors; 19 | } 20 | 21 | #region ITableEntriesSnapshotFactory members 22 | 23 | public override int CurrentVersionNumber => this.CurrentSnapshot.VersionNumber; 24 | 25 | public override ITableEntriesSnapshot GetCurrentSnapshot() => this.CurrentSnapshot; 26 | 27 | public override void Dispose() 28 | { 29 | } 30 | 31 | public override ITableEntriesSnapshot GetSnapshot(int versionNumber) 32 | { 33 | // In theory the snapshot could change in the middle of the return statement so snap the snapshot just to be safe. 34 | var snapshot = this.CurrentSnapshot; 35 | return (versionNumber == snapshot.VersionNumber) ? snapshot : null; 36 | } 37 | 38 | #endregion 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Table/LintingErrorsSnapshot.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.VisualStudio.Shell.Interop; 2 | using Microsoft.VisualStudio.Shell.TableControl; 3 | using Microsoft.VisualStudio.Shell.TableManager; 4 | using Microsoft.VisualStudio.Text; 5 | using System.Collections.Generic; 6 | using System.Diagnostics; 7 | 8 | namespace FSharpLintVs 9 | { 10 | public class LintingErrorsSnapshot : WpfTableEntriesSnapshotBase 11 | { 12 | public ITextDocument Document { get; } 13 | 14 | public override int VersionNumber { get; } 15 | 16 | public override int Count => this.Errors.Count; 17 | 18 | public const string ErrorCategory = "Lint"; 19 | public const string ToolName = "FSharpLint"; 20 | 21 | // We're not using an immutable list here but we cannot modify the list in any way once we've published the snapshot. 22 | public List Errors { get; } 23 | 24 | public LintingErrorsSnapshot NextSnapshot; 25 | 26 | public LintingErrorsSnapshot(ITextDocument document, int version) 27 | { 28 | Document = document; 29 | VersionNumber = version; 30 | Errors = new List(); 31 | } 32 | 33 | public override int IndexOf(int currentIndex, ITableEntriesSnapshot newerSnapshot) 34 | { 35 | // This and TranslateTo() are used to map errors from one snapshot to a different one (that way the error list can do things like maintain the selection on an error 36 | // even when the snapshot containing the error is replaced by a new one). 37 | // 38 | // You only need to implement Identity() or TranslateTo() and, of the two, TranslateTo() is more efficient for the error list to use. 39 | 40 | // Map currentIndex to the corresponding index in newerSnapshot (and keep doing it until either 41 | // we run out of snapshots, we reach newerSnapshot, or the index can no longer be mapped forward). 42 | var currentSnapshot = this; 43 | do 44 | { 45 | Debug.Assert(currentIndex >= 0); 46 | Debug.Assert(currentIndex < currentSnapshot.Count); 47 | 48 | currentIndex = currentSnapshot.Errors[currentIndex].NextIndex; 49 | 50 | currentSnapshot = currentSnapshot.NextSnapshot; 51 | } 52 | while ((currentSnapshot != null) && (currentSnapshot != newerSnapshot) && (currentIndex >= 0)); 53 | 54 | return currentIndex; 55 | } 56 | 57 | public override bool TryGetValue(int index, string columnName, out object content) 58 | { 59 | if (index >= 0 && index < this.Errors.Count) 60 | { 61 | var err = this.Errors[index]; 62 | 63 | switch (columnName) 64 | { 65 | case StandardTableKeyNames.DocumentName: 66 | { 67 | // We return the full file path here. The UI handles displaying only the Path.GetFileName(). 68 | content = Document.FilePath; 69 | return true; 70 | } 71 | 72 | case StandardTableKeyNames.ErrorCategory: 73 | { 74 | content = ErrorCategory; 75 | return true; 76 | } 77 | 78 | case StandardTableKeyNames.ErrorSource: 79 | { 80 | content = ErrorSource.Other; 81 | return true; 82 | } 83 | 84 | case StandardTableKeyNames.Line: 85 | { 86 | // Line and column numbers are 0-based (the UI that displays the line/column number will add one to the value returned here). 87 | content = err.Line; 88 | return true; 89 | } 90 | 91 | case StandardTableKeyNames.Column: 92 | { 93 | content = err.Column; 94 | return true; 95 | } 96 | 97 | case StandardTableKeyNames.Text: 98 | { 99 | content = err.Message; 100 | return true; 101 | } 102 | 103 | case StandardTableKeyNames2.TextInlines: 104 | { 105 | //content = "?"; 106 | // Do we have detailed inline text? 107 | content = ""; 108 | return false; 109 | } 110 | 111 | case StandardTableKeyNames.ErrorSeverity: 112 | { 113 | content = __VSERRORCATEGORY.EC_WARNING; 114 | return true; 115 | } 116 | 117 | case StandardTableKeyNames.BuildTool: 118 | { 119 | content = ToolName; 120 | return true; 121 | } 122 | 123 | case StandardTableKeyNames.ErrorCode: 124 | { 125 | content = err.Identifier; 126 | return true; 127 | } 128 | 129 | case StandardTableKeyNames.ErrorCodeToolTip: 130 | { 131 | content = err.Name; 132 | return true; 133 | } 134 | 135 | case StandardTableKeyNames.HelpLink: 136 | { 137 | content = err.HelpUrl; 138 | return true; 139 | } 140 | 141 | case StandardTableKeyNames.ProjectName: 142 | { 143 | content = err.Project.ProjectName; 144 | return true; 145 | } 146 | 147 | case StandardTableKeyNames.ProjectGuid: 148 | { 149 | content = err.Project.ProjectGuid; 150 | return true; 151 | } 152 | } 153 | 154 | } 155 | 156 | content = null; 157 | return false; 158 | } 159 | 160 | public override bool CanCreateDetailsContent(int index) 161 | { 162 | return false; 163 | } 164 | 165 | public override bool TryCreateDetailsStringContent(int index, out string content) 166 | { 167 | content = this.Errors[index].Tooltip; 168 | return content != null; 169 | } 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /src/source.extension.vsixmanifest: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | F# Lint 6 | F# source code linting using FSharpLint. 7 | https://github.com/deviousasti/fsharp-linting-for-vs 8 | Resources\License.txt 9 | Resources\ReleaseNotes.html 10 | Resources\logo.png 11 | Resources\logo.png 12 | fsharp, linting 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | --------------------------------------------------------------------------------