Skip to content

Commit

Permalink
Merge pull request #9 from vasily-kirichenko/toupper
Browse files Browse the repository at this point in the history
Make ProposeUppercaseLabel code fix work on all symbol uses
  • Loading branch information
forki authored Dec 27, 2016
2 parents b4a00ce + 193a356 commit 99eee9c
Show file tree
Hide file tree
Showing 5 changed files with 145 additions and 87 deletions.
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

0 comments on commit 99eee9c

Please sign in to comment.