Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make ProposeUppercaseLabel code fix work on all symbol uses #9

Merged
merged 1 commit into from
Dec 27, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 69 additions & 15 deletions vsintegration/src/FSharp.Editor/CodeFix/ProposeUppercaseLabel.fs
Original file line number Diff line number Diff line change
Expand Up @@ -11,31 +11,85 @@ open Microsoft.CodeAnalysis
open Microsoft.CodeAnalysis.Text
open Microsoft.CodeAnalysis.CodeFixes
open Microsoft.CodeAnalysis.CodeActions
open Microsoft.VisualStudio.FSharp.LanguageService
open Microsoft.VisualStudio.Text.Tagging
open Microsoft.VisualStudio.Text.Formatting
open Microsoft.VisualStudio.Shell.Interop

open Microsoft.FSharp.Compiler
open Microsoft.FSharp.Compiler.Parser
open Microsoft.FSharp.Compiler.Range
open Microsoft.FSharp.Compiler.SourceCodeServices

open System.Windows.Documents

[<ExportCodeFixProvider(FSharpCommonConstants.FSharpLanguageName, Name = "ProposeUpperCaseLabel"); Shared>]
type internal FSharpProposeUpperCaseLabelCodeFixProvider() =
type internal FSharpProposeUpperCaseLabelCodeFixProvider
[<ImportingConstructor>]
(
checkerProvider: FSharpCheckerProvider,
projectInfoManager: ProjectInfoManager
) =
inherit CodeFixProvider()
let fixableDiagnosticIds = ["FS0053"]

let createCodeFix (title: string, context: CodeFixContext, textChange: TextChange) =
CodeAction.Create(
title,
(fun (cancellationToken: CancellationToken) ->
async {
let! sourceText = context.Document.GetTextAsync()
return context.Document.WithText(sourceText.WithChanges(textChange))
} |> CommonRoslynHelpers.StartAsyncAsTask(cancellationToken)),
title)

override __.FixableDiagnosticIds = fixableDiagnosticIds.ToImmutableArray()

override __.RegisterCodeFixesAsync context : Task =
async {
let diagnostics = (context.Diagnostics |> Seq.filter (fun x -> fixableDiagnosticIds |> List.contains x.Id)).ToImmutableArray()
if context.Span.Length > 0 then
let! sourceText = context.Document.GetTextAsync(context.CancellationToken)
let document = context.Document
let! sourceText = document.GetTextAsync(context.CancellationToken)
let originalText = sourceText.ToString(context.Span)
if originalText.Length > 0 then
let newText = originalText.[0].ToString().ToUpper() + originalText.Substring(1)
context.RegisterCodeFix(createCodeFix(FSComp.SR.replaceWithSuggestion newText, context, TextChange(context.Span, newText)), diagnostics)
match projectInfoManager.TryGetOptionsForEditingDocumentOrProject context.Document with
| Some options ->
let! sourceText = context.Document.GetTextAsync(context.CancellationToken)
let defines = CompilerEnvironment.GetCompilationDefinesForEditing(document.Name, options.OtherOptions |> Seq.toList)
match CommonHelpers.getSymbolAtPosition(document.Id, sourceText, context.Span.Start, document.FilePath, defines, SymbolLookupKind.Fuzzy) with
| Some symbol ->
let! textVersion = document.GetTextVersionAsync(context.CancellationToken)
let checker = checkerProvider.Checker
let! _, checkFileAnswer = checker.ParseAndCheckFileInProject(context.Document.FilePath, textVersion.GetHashCode(), sourceText.ToString(), options)
match checkFileAnswer with
| FSharpCheckFileAnswer.Aborted -> ()
| FSharpCheckFileAnswer.Succeeded checkFileResults ->
let textLine = sourceText.Lines.GetLineFromPosition(context.Span.Start)
let textLinePos = sourceText.Lines.GetLinePosition(context.Span.Start)
let fcsTextLineNumber = textLinePos.Line + 1
let! symbolUse = checkFileResults.GetSymbolUseAtLocation(fcsTextLineNumber, symbol.RightColumn, textLine.Text.ToString(), [symbol.Text])
match symbolUse with
| Some symbolUse ->
match symbolUse.GetDeclarationLocation(document) with
| None -> ()
| Some declLoc ->
let newText = originalText.[0].ToString().ToUpper() + originalText.Substring(1)
let title = FSComp.SR.replaceWithSuggestion newText
// defer finding all symbol uses throughout the solution until the code fix action is executed
let codeFix =
CodeAction.Create(
title,
(fun (cancellationToken: CancellationToken) ->
async {
let! symbolUsesByDocumentId =
SymbolHelpers.getSymbolUsesInSolution(symbolUse.Symbol, declLoc, checkFileResults, projectInfoManager, checker, document.Project.Solution)

let mutable solution = document.Project.Solution

for KeyValue(documentId, symbolUses) in symbolUsesByDocumentId do
let document = document.Project.Solution.GetDocument(documentId)
let! sourceText = document.GetTextAsync(cancellationToken)
let mutable sourceText = sourceText
for symbolUse in symbolUses do
let textSpan = CommonHelpers.fixupSpan(sourceText, CommonRoslynHelpers.FSharpRangeToTextSpan(sourceText, symbolUse.RangeAlternate))
sourceText <- sourceText.Replace(textSpan, newText)
solution <- solution.WithDocumentText(documentId, sourceText)
return solution
} |> CommonRoslynHelpers.StartAsyncAsTask(cancellationToken)),
title)
let diagnostics = (context.Diagnostics |> Seq.filter (fun x -> fixableDiagnosticIds |> List.contains x.Id)).ToImmutableArray()
context.RegisterCodeFix(codeFix, diagnostics)
| None -> ()
| None -> ()
| _ -> ()
} |> CommonRoslynHelpers.StartAsyncUnitAsTask(context.CancellationToken)
30 changes: 3 additions & 27 deletions vsintegration/src/FSharp.Editor/Common/CommonHelpers.fs
Original file line number Diff line number Diff line change
Expand Up @@ -319,7 +319,7 @@ module internal CommonHelpers =
try
let textLine = sourceText.Lines.GetLineFromPosition(position)
let textLinePos = sourceText.Lines.GetLinePosition(position)
let lineNumber = textLinePos.Line + 1 // FCS line number
let lineNumber = textLinePos.Line
let sourceTokenizer = FSharpSourceTokenizer(defines, Some fileName)
let lines = sourceText.Lines
// We keep incremental data per-document. When text changes we correlate text line-by-line (by hash codes of lines)
Expand All @@ -328,11 +328,11 @@ module internal CommonHelpers =
// Go backwards to find the last cached scanned line that is valid
let scanStartLine =
let mutable i = lineNumber
while i > 0 && (match sourceTextData.[i-1] with Some data -> not (data.IsValid(lines.[i])) | None -> true) do
while i > 0 && (match sourceTextData.[i] with Some data -> not (data.IsValid(lines.[i])) | None -> true) do
i <- i - 1
i

let lexState = if scanStartLine = 0 then 0L else sourceTextData.[scanStartLine - 1].Value.LexStateAtEndOfLine
let lexState = if scanStartLine = 0 then 0L else sourceTextData.[scanStartLine].Value.LexStateAtEndOfLine
let lineContents = textLine.Text.ToString(textLine.Span)

let lineData =
Expand Down Expand Up @@ -477,30 +477,6 @@ module internal Extensions =

isPrivate && declaredInTheFile

let glyphMajorToRoslynGlyph = function
| GlyphMajor.Class -> Glyph.ClassPublic
| GlyphMajor.Constant -> Glyph.ConstantPublic
| GlyphMajor.Delegate -> Glyph.DelegatePublic
| GlyphMajor.Enum -> Glyph.EnumPublic
| GlyphMajor.EnumMember -> Glyph.FieldPublic
| GlyphMajor.Event -> Glyph.EventPublic
| GlyphMajor.Exception -> Glyph.ClassPublic
| GlyphMajor.FieldBlue -> Glyph.FieldPublic
| GlyphMajor.Interface -> Glyph.InterfacePublic
| GlyphMajor.Method -> Glyph.MethodPublic
| GlyphMajor.Method2 -> Glyph.MethodPublic
| GlyphMajor.Module -> Glyph.ModulePublic
| GlyphMajor.NameSpace -> Glyph.Namespace
| GlyphMajor.Property -> Glyph.PropertyPublic
| GlyphMajor.Struct -> Glyph.StructurePublic
| GlyphMajor.Typedef -> Glyph.ClassPublic
| GlyphMajor.Type -> Glyph.ClassPublic
| GlyphMajor.Union -> Glyph.EnumPublic
| GlyphMajor.Variable -> Glyph.FieldPublic
| GlyphMajor.ValueType -> Glyph.StructurePublic
| GlyphMajor.Error -> Glyph.Error
| _ -> Glyph.None

type Async<'a> with
/// Creates an asynchronous workflow that runs the asynchronous workflow given as an argument at most once.
/// When the returned workflow is started for the second time, it reuses the result of the previous execution.
Expand Down
61 changes: 61 additions & 0 deletions vsintegration/src/FSharp.Editor/Common/SymbolHelpers.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

namespace Microsoft.VisualStudio.FSharp.Editor

open System
open System.Collections.Generic
open System.Collections.Immutable
open System.Threading
open System.Threading.Tasks
open System.Runtime.CompilerServices

open Microsoft.CodeAnalysis
open Microsoft.CodeAnalysis.Classification
open Microsoft.CodeAnalysis.Text

open Microsoft.VisualStudio.FSharp.LanguageService
open Microsoft.FSharp.Compiler
open Microsoft.FSharp.Compiler.SourceCodeServices
open Microsoft.FSharp.Compiler.SourceCodeServices.ItemDescriptionIcons


module internal SymbolHelpers =
let getSymbolUsesInSolution (symbol: FSharpSymbol, declLoc: SymbolDeclarationLocation, checkFileResults: FSharpCheckFileResults,
projectInfoManager: ProjectInfoManager, checker: FSharpChecker, solution: Solution) =
async {
let! symbolUses =
match declLoc with
| SymbolDeclarationLocation.CurrentDocument ->
checkFileResults.GetUsesOfSymbolInFile(symbol)
| SymbolDeclarationLocation.Projects (projects, isInternalToProject) ->
let projects =
if isInternalToProject then projects
else
[ for project in projects do
yield project
yield! project.GetDependentProjects() ]
|> List.distinctBy (fun x -> x.Id)

projects
|> Seq.map (fun project ->
async {
match projectInfoManager.TryGetOptionsForProject(project.Id) with
| Some options ->
let! projectCheckResults = checker.ParseAndCheckProject(options)
return! projectCheckResults.GetUsesOfSymbol(symbol)
| None -> return [||]
})
|> Async.Parallel
|> Async.Map Array.concat

return
(symbolUses
|> Seq.collect (fun symbolUse ->
solution.GetDocumentIdsWithFilePath(symbolUse.FileName) |> Seq.map (fun id -> id, symbolUse))
|> Seq.groupBy fst
).ToImmutableDictionary(
(fun (id, _) -> id),
fun (_, xs) -> xs |> Seq.map snd |> Seq.toArray)
}


3 changes: 2 additions & 1 deletion vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -->
<Project ToolsVersion="15.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
Expand Down Expand Up @@ -38,6 +38,7 @@
<Compile Include="Common\Logging.fs" />
<Compile Include="Common\ContentType.fs" />
<Compile Include="Common\LanguageService.fs" />
<Compile Include="Common\SymbolHelpers.fs" />
<Compile Include="Common\AssemblyContentProvider.fs" />
<Compile Include="Classification\ColorizationService.fs" />
<Compile Include="Formatting\BraceMatchingService.fs" />
Expand Down
54 changes: 10 additions & 44 deletions vsintegration/src/FSharp.Editor/InlineRename/InlineRenameService.fs
Original file line number Diff line number Diff line change
Expand Up @@ -91,42 +91,9 @@ type internal InlineRenameInfo
let span = CommonRoslynHelpers.FSharpRangeToTextSpan(sourceText, symbolUse.RangeAlternate)
CommonHelpers.fixupSpan(sourceText, span)

let symbolUses =
async {
let! symbolUses =
match declLoc with
| SymbolDeclarationLocation.CurrentDocument ->
checkFileResults.GetUsesOfSymbolInFile(symbolUse.Symbol)
| SymbolDeclarationLocation.Projects (projects, isInternalToProject) ->
let projects =
if isInternalToProject then projects
else
[ for project in projects do
yield project
yield! project.GetDependentProjects() ]
|> List.distinctBy (fun x -> x.Id)

projects
|> Seq.map (fun project ->
async {
match projectInfoManager.TryGetOptionsForProject(project.Id) with
| Some options ->
let! projectCheckResults = checker.ParseAndCheckProject(options)
return! projectCheckResults.GetUsesOfSymbol(symbolUse.Symbol)
| None -> return [||]
})
|> Async.Parallel
|> Async.Map Array.concat

return
(symbolUses
|> Seq.collect (fun symbolUse ->
document.Project.Solution.GetDocumentIdsWithFilePath(symbolUse.FileName) |> Seq.map (fun id -> id, symbolUse))
|> Seq.groupBy fst
).ToImmutableDictionary(
(fun (id, _) -> id),
fun (_, xs) -> xs |> Seq.map snd |> Seq.toArray)
} |> Async.Cache
let symbolUses =
SymbolHelpers.getSymbolUsesInSolution(symbolUse.Symbol, declLoc, checkFileResults, projectInfoManager, checker, document.Project.Solution)
|> Async.Cache

interface IInlineRenameInfo with
member __.CanRename = true
Expand Down Expand Up @@ -191,15 +158,14 @@ type internal InlineRenameService
match checkFileAnswer with
| FSharpCheckFileAnswer.Aborted -> return FailureInlineRenameInfo.Instance
| FSharpCheckFileAnswer.Succeeded(checkFileResults) ->

let! symbolUse = checkFileResults.GetSymbolUseAtLocation(fcsTextLineNumber, symbol.RightColumn, textLine.Text.ToString(), [symbol.Text])

match symbolUse with
| Some symbolUse ->
match symbolUse.GetDeclarationLocation(document) with
| Some declLoc -> return InlineRenameInfo(checker, projectInfoManager, document, sourceText, symbolUse, declLoc, checkFileResults) :> IInlineRenameInfo
let! symbolUse = checkFileResults.GetSymbolUseAtLocation(fcsTextLineNumber, symbol.RightColumn, textLine.Text.ToString(), [symbol.Text])
match symbolUse with
| Some symbolUse ->
match symbolUse.GetDeclarationLocation(document) with
| Some declLoc -> return InlineRenameInfo(checker, projectInfoManager, document, sourceText, symbolUse, declLoc, checkFileResults) :> IInlineRenameInfo
| _ -> return FailureInlineRenameInfo.Instance
| _ -> return FailureInlineRenameInfo.Instance
| _ -> return FailureInlineRenameInfo.Instance
| None -> return FailureInlineRenameInfo.Instance
}

Expand Down