diff --git a/CommonHelpers.fs b/CommonHelpers.fs index d1cce824524..32fcbf76dc1 100644 --- a/CommonHelpers.fs +++ b/CommonHelpers.fs @@ -60,7 +60,7 @@ module CommonHelpers = | FSharpTokenColorKind.PreprocessorKeyword -> ClassificationTypeNames.PreprocessorKeyword | FSharpTokenColorKind.Operator -> ClassificationTypeNames.Operator | FSharpTokenColorKind.TypeName -> ClassificationTypeNames.ClassName - | FSharpTokenColorKind.Default + | FSharpTokenColorKind.Default | _ -> ClassificationTypeNames.Text let private scanSourceLine(sourceTokenizer: FSharpSourceTokenizer, textLine: TextLine, lineContents: string, lexState: FSharpTokenizerLexState) : SourceLineData = @@ -71,11 +71,7 @@ module CommonHelpers = let tokenInfoOption, nextLexState = lineTokenizer.ScanToken(lexState.Value) lexState.Value <- nextLexState if tokenInfoOption.IsSome then - let classificationType = - if tokenInfoOption.Value.CharClass = FSharpTokenCharKind.WhiteSpace then - ClassificationTypeNames.WhiteSpace - else - compilerTokenToRoslynToken(tokenInfoOption.Value.ColorClass) + let classificationType = compilerTokenToRoslynToken(tokenInfoOption.Value.ColorClass) for i = tokenInfoOption.Value.LeftColumn to tokenInfoOption.Value.RightColumn do Array.set colorMap i classificationType tokenInfoOption @@ -163,21 +159,14 @@ module CommonHelpers = Assert.Exception(ex) List() - let tryClassifyAtPosition (documentKey, sourceText: SourceText, filePath, defines, position: int, includeRightColumn: bool, cancellationToken) = + let tryClassifyAtPosition (documentKey, sourceText: SourceText, filePath, defines, position: int, cancellationToken) = let textLine = sourceText.Lines.GetLineFromPosition(position) let textLinePos = sourceText.Lines.GetLinePosition(position) let textLineColumn = textLinePos.Character let classifiedSpanOption = getColorizationData(documentKey, sourceText, textLine.Span, Some(filePath), defines, cancellationToken) - |> Seq.tryFind(fun classifiedSpan -> - if includeRightColumn then - classifiedSpan.ClassificationType <> ClassificationTypeNames.WhiteSpace && - (classifiedSpan.TextSpan.Contains(position) || - // TextSpan.Contains returns `false` for `position` equals its right bound, - // so we have to check if it contains `position - 1`. - (position > 0 && classifiedSpan.TextSpan.Contains(position - 1))) - else classifiedSpan.TextSpan.Contains(position)) + |> Seq.tryFind(fun classifiedSpan -> classifiedSpan.TextSpan.Contains(position)) match classifiedSpanOption with | Some(classifiedSpan) -> @@ -201,6 +190,123 @@ module CommonHelpers = | _ -> None | _ -> None + /// Fix invalid span if it appears to have redundant suffix and prefix. + let fixupSpan (sourceText: SourceText, span: TextSpan) : TextSpan = + let text = sourceText.GetSubText(span).ToString() + match text.LastIndexOf '.' with + | -1 | 0 -> span + | index -> TextSpan(span.Start + index + 1, text.Length - index - 1) + + let glyphMajorToRoslynGlyph = function + | GlyphMajor.Class + | GlyphMajor.Typedef + | GlyphMajor.Type + | GlyphMajor.Exception -> Glyph.ClassPublic + | GlyphMajor.Constant -> Glyph.ConstantPublic + | GlyphMajor.Delegate -> Glyph.DelegatePublic + | GlyphMajor.Union + | GlyphMajor.Enum -> Glyph.EnumPublic + | GlyphMajor.EnumMember + | GlyphMajor.Variable + | GlyphMajor.FieldBlue -> Glyph.FieldPublic + | GlyphMajor.Event -> Glyph.EventPublic + | GlyphMajor.Interface -> Glyph.InterfacePublic + | GlyphMajor.Method + | GlyphMajor.Method2 -> Glyph.MethodPublic + | GlyphMajor.Module -> Glyph.ModulePublic + | GlyphMajor.NameSpace -> Glyph.Namespace + | GlyphMajor.Property -> Glyph.PropertyPublic + | GlyphMajor.Struct + | GlyphMajor.ValueType -> Glyph.StructurePublic + | GlyphMajor.Error -> Glyph.Error + | _ -> Glyph.None + +[] +type internal SymbolDeclarationLocation = + | CurrentDocument + | Projects of Project list * isLocalForProject: bool + +[] +module internal Extensions = + open System + open System.IO + + type Path with + static member GetFullPathSafe path = + try Path.GetFullPath path + with _ -> path + + type FSharpSymbol with + member this.IsInternalToProject = + match this with + | :? FSharpParameter -> true + | :? FSharpMemberOrFunctionOrValue as m -> not m.IsModuleValueOrMember || not m.Accessibility.IsPublic + | :? FSharpEntity as m -> not m.Accessibility.IsPublic + | :? FSharpGenericParameter -> true + | :? FSharpUnionCase as m -> not m.Accessibility.IsPublic + | :? FSharpField as m -> not m.Accessibility.IsPublic + | _ -> false + + type FSharpSymbolUse with + member this.GetDeclarationLocation (currentDocument: Document) : SymbolDeclarationLocation option = + if this.IsPrivateToFile then + Some SymbolDeclarationLocation.CurrentDocument + else + let isSymbolLocalForProject = this.Symbol.IsInternalToProject + + let declarationLocation = + match this.Symbol.ImplementationLocation with + | Some x -> Some x + | None -> this.Symbol.DeclarationLocation + + match declarationLocation with + | Some loc -> + let filePath = Path.GetFullPathSafe loc.FileName + let isScript = String.Equals(Path.GetExtension(filePath), ".fsx", StringComparison.OrdinalIgnoreCase) + if isScript && filePath = currentDocument.FilePath then + Some SymbolDeclarationLocation.CurrentDocument + elif isScript then + // The standalone script might include other files via '#load' + // These files appear in project options and the standalone file + // should be treated as an individual project + Some (SymbolDeclarationLocation.Projects ([currentDocument.Project], isSymbolLocalForProject)) + else + let projects = + currentDocument.Project.Solution.GetDocumentIdsWithFilePath(currentDocument.FilePath) + |> Seq.map (fun x -> x.ProjectId) + |> Seq.distinct + |> Seq.map currentDocument.Project.Solution.GetProject + |> Seq.toList + match projects with + | [] -> None + | projects -> Some (SymbolDeclarationLocation.Projects (projects, isSymbolLocalForProject)) + | None -> None + + member this.IsPrivateToFile = + let isPrivate = + match this.Symbol with + | :? FSharpMemberOrFunctionOrValue as m -> not m.IsModuleValueOrMember + | :? FSharpEntity as m -> m.Accessibility.IsPrivate + | :? FSharpGenericParameter -> true + | :? FSharpUnionCase as m -> m.Accessibility.IsPrivate + | :? FSharpField as m -> m.Accessibility.IsPrivate + | _ -> false + + let declarationLocation = + match this.Symbol.SignatureLocation with + | Some x -> Some x + | _ -> + match this.Symbol.DeclarationLocation with + | Some x -> Some x + | _ -> this.Symbol.ImplementationLocation + + let declaredInTheFile = + match declarationLocation with + | Some declRange -> declRange.FileName = this.RangeAlternate.FileName + | _ -> false + + isPrivate && declaredInTheFile + let glyphMajorToRoslynGlyph = function | GlyphMajor.Class -> Glyph.ClassPublic | GlyphMajor.Constant -> Glyph.ConstantPublic @@ -223,4 +329,5 @@ module CommonHelpers = | GlyphMajor.Variable -> Glyph.FieldPublic | GlyphMajor.ValueType -> Glyph.StructurePublic | GlyphMajor.Error -> Glyph.Error - | _ -> Glyph.None \ No newline at end of file + | _ -> Glyph.None + diff --git a/CommonRoslynHelpers.fs b/CommonRoslynHelpers.fs index ed4a279dc0d..b7eeb6b1c34 100644 --- a/CommonRoslynHelpers.fs +++ b/CommonRoslynHelpers.fs @@ -20,6 +20,12 @@ module internal CommonRoslynHelpers = let endPosition = sourceText.Lines.[range.EndLine - 1].Start + range.EndColumn TextSpan(startPosition, endPosition - startPosition) + let TryFSharpRangeToTextSpan(sourceText: SourceText, range: range) : TextSpan option = + try Some(FSharpRangeToTextSpan(sourceText, range)) + with e -> + //Assert.Exception(e) + None + let GetCompletedTaskResult(task: Task<'TResult>) = if task.Status = TaskStatus.RanToCompletion then task.Result @@ -53,3 +59,12 @@ module internal CommonRoslynHelpers = let severity = if error.Severity = FSharpErrorSeverity.Error then DiagnosticSeverity.Error else DiagnosticSeverity.Warning let descriptor = new DiagnosticDescriptor(id, emptyString, description, error.Subcategory, severity, true, emptyString, String.Empty, null) Diagnostic.Create(descriptor, location) + +[] +module internal RoslynExtensions = + type Project with + /// The list of all other projects within the same solution that reference this project. + member this.GetDependentProjects() = + [ for project in this.Solution.Projects do + if project.ProjectReferences |> Seq.exists (fun ref -> ref.ProjectId = this.Id) then + yield project ] \ No newline at end of file diff --git a/FSharp.Editor.fsproj b/FSharp.Editor.fsproj index 48b93bb12cb..6d4d1d37fcc 100644 --- a/FSharp.Editor.fsproj +++ b/FSharp.Editor.fsproj @@ -1,4 +1,4 @@ - + @@ -18,8 +18,7 @@ LIBRARY v4.6 $(NoWarn);75 - false - $(OtherFlags) --warnon:1182 --subsystemversion:6.00 + false $(OtherFlags) --warnon:1182 --subsystemversion:6.00 true false false diff --git a/Navigation/FindReferencesService.fs b/Navigation/FindReferencesService.fs new file mode 100644 index 00000000000..013c8d3c1be --- /dev/null +++ b/Navigation/FindReferencesService.fs @@ -0,0 +1,157 @@ +// 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.Threading +open System.Collections.Immutable +open System.Composition + +open Microsoft.CodeAnalysis +open Microsoft.CodeAnalysis.Host.Mef +open Microsoft.CodeAnalysis.Editor +open Microsoft.CodeAnalysis.Editor.Host +open Microsoft.CodeAnalysis.Navigation +open Microsoft.CodeAnalysis.FindSymbols +open Microsoft.CodeAnalysis.FindReferences + +open Microsoft.VisualStudio.FSharp.LanguageService + +open Microsoft.FSharp.Compiler.Range +open Microsoft.FSharp.Compiler.SourceCodeServices + +[, FSharpCommonConstants.FSharpLanguageName); Shared>] +type internal FSharpFindReferencesService + [] + ( + checkerProvider: FSharpCheckerProvider, + projectInfoManager: ProjectInfoManager + ) = + + // File can be included in more than one project, hence single `range` may results with multiple `Document`s. + let rangeToDocumentSpans (solution: Solution, range: range, cancellationToken: CancellationToken) = + async { + if range.Start = range.End then return [] + else + let! spans = + solution.GetDocumentIdsWithFilePath(range.FileName) + |> Seq.map (fun documentId -> + async { + let doc = solution.GetDocument(documentId) + let! sourceText = doc.GetTextAsync(cancellationToken) |> Async.AwaitTask + match CommonRoslynHelpers.TryFSharpRangeToTextSpan(sourceText, range) with + | Some span -> + let span = CommonHelpers.fixupSpan(sourceText, span) + return Some (DocumentSpan(doc, span)) + | None -> return None + }) + |> Async.Parallel + return spans |> Array.choose id |> Array.toList + } + + let findReferencedSymbolsAsync(document: Document, position: int, context: FindReferencesContext) : Async = + async { + let! sourceText = document.GetTextAsync(context.CancellationToken) |> Async.AwaitTask + let! textVersion = document.GetTextVersionAsync(context.CancellationToken) |> Async.AwaitTask + let checker = checkerProvider.Checker + let! options = projectInfoManager.TryGetOptionsForDocumentOrProject(document) + match options with + | Some options -> + let! _parseResults, checkResultsAnswer = checker.ParseAndCheckFileInProject(document.FilePath, hash textVersion, sourceText.ToString(), options) + let checkFileResults = + match checkResultsAnswer with + | FSharpCheckFileAnswer.Aborted -> failwith "Compilation isn't complete yet" + | FSharpCheckFileAnswer.Succeeded(results) -> results + + let textLine = sourceText.Lines.GetLineFromPosition(position).ToString() + let lineNumber = sourceText.Lines.GetLinePosition(position).Line + 1 + let defines = CompilerEnvironment.GetCompilationDefinesForEditing(document.FilePath, options.OtherOptions |> Seq.toList) + + match CommonHelpers.tryClassifyAtPosition(document.Id, sourceText, document.FilePath, defines, position, context.CancellationToken) with + | Some (islandColumn, qualifiers, _) -> + let! symbolUse = checkFileResults.GetSymbolUseAtLocation(lineNumber, islandColumn, textLine, qualifiers) + match symbolUse with + | Some symbolUse -> + let! declaration = checkFileResults.GetDeclarationLocationAlternate (lineNumber, islandColumn, textLine, qualifiers, false) + let declarationRange = + match declaration with + | FSharpFindDeclResult.DeclFound range -> Some range + | _ -> None + + let! declarationSpans = + match declarationRange with + | Some range -> rangeToDocumentSpans(document.Project.Solution, range, context.CancellationToken) + | None -> async.Return [] + + let definitionItems = + match declarationSpans with + | [] -> + [ DefinitionItem.CreateNonNavigableItem( + ImmutableArray.Empty, + [TaggedText(TextTags.Text, symbolUse.Symbol.FullName)].ToImmutableArray(), + [TaggedText(TextTags.Assembly, symbolUse.Symbol.Assembly.SimpleName)].ToImmutableArray()) ] + | _ -> + declarationSpans + |> List.map (fun span -> + DefinitionItem.Create( + ImmutableArray.Empty, + [TaggedText(TextTags.Text, symbolUse.Symbol.FullName)].ToImmutableArray(), + span)) + + for definitionItem in definitionItems do + do! context.OnDefinitionFoundAsync(definitionItem) |> Async.AwaitTask + + let! symbolUses = + match symbolUse.GetDeclarationLocation document with + | Some SymbolDeclarationLocation.CurrentDocument -> + checkFileResults.GetUsesOfSymbolInFile(symbolUse.Symbol) + | scope -> + let projectsToCheck = + match scope with + | Some (SymbolDeclarationLocation.Projects (declProjects, false)) -> + declProjects |> List.collect (fun x -> x.GetDependentProjects()) + | Some (SymbolDeclarationLocation.Projects (declProjects, true)) -> declProjects + // The symbol is declared in .NET framework, an external assembly or in a C# project within the solution. + // In order to find all its usages we have to check all F# projects. + | _ -> Seq.toList document.Project.Solution.Projects + + async { + let! symbolUses = + projectsToCheck + |> 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 + + return symbolUses |> Array.concat + } + + for symbolUse in symbolUses do + match declarationRange with + | Some declRange when declRange = symbolUse.RangeAlternate -> () + | _ -> + let! referenceDocSpans = rangeToDocumentSpans(document.Project.Solution, symbolUse.RangeAlternate, context.CancellationToken) + match referenceDocSpans with + | [] -> () + | _ -> + for referenceDocSpan in referenceDocSpans do + for definitionItem in definitionItems do + let referenceItem = SourceReferenceItem(definitionItem, referenceDocSpan) + do! context.OnReferenceFoundAsync(referenceItem) |> Async.AwaitTask + | None -> () + | None -> () + | None -> () + + do! context.OnCompletedAsync() |> Async.AwaitTask + } + + interface IStreamingFindReferencesService with + member __.FindReferencesAsync(document, position, context) = + findReferencedSymbolsAsync(document, position, context) + |> CommonRoslynHelpers.StartAsyncUnitAsTask(context.CancellationToken) + \ No newline at end of file diff --git a/Navigation/GoToDefinitionService.fs b/Navigation/GoToDefinitionService.fs index 3e2027550bd..84e65105127 100644 --- a/Navigation/GoToDefinitionService.fs +++ b/Navigation/GoToDefinitionService.fs @@ -53,7 +53,6 @@ type internal FSharpGoToDefinitionService let textLine = sourceText.Lines.GetLineFromPosition(position) let textLinePos = sourceText.Lines.GetLinePosition(position) let fcsTextLineNumber = textLinePos.Line + 1 // Roslyn line numbers are zero-based, FSharp.Compiler.Service line numbers are 1-based - match CommonHelpers.tryClassifyAtPosition(documentKey, sourceText, filePath, defines, position, true, cancellationToken) with | Some (islandColumn, qualifiers, _) -> let! _parseResults, checkFileAnswer = checker.ParseAndCheckFileInProject(filePath, textVersionHash, sourceText.ToString(), options) diff --git a/QuickInfo/QuickInfoProvider.fs b/QuickInfo/QuickInfoProvider.fs index feb219503f1..467ade58cb2 100644 --- a/QuickInfo/QuickInfoProvider.fs +++ b/QuickInfo/QuickInfoProvider.fs @@ -80,9 +80,17 @@ type internal FSharpQuickInfoProvider let textLine = sourceText.Lines.GetLineFromPosition(position) let textLineNumber = textLine.LineNumber + 1 // Roslyn line numbers are zero-based + let textLinePos = sourceText.Lines.GetLinePosition(position) + let textLineColumn = textLinePos.Character //let qualifyingNames, partialName = QuickParse.GetPartialLongNameEx(textLine.ToString(), textLineColumn - 1) let defines = CompilerEnvironment.GetCompilationDefinesForEditing(filePath, options.OtherOptions |> Seq.toList) - let quickParseInfo = CommonHelpers.tryClassifyAtPosition(documentId, sourceText, filePath, defines, position, false, cancellationToken) + let tryClassifyAtPosition position = + CommonHelpers.tryClassifyAtPosition(documentId, sourceText, filePath, defines, position, cancellationToken) + + let quickParseInfo = + match tryClassifyAtPosition position with + | None when textLineColumn > 0 -> tryClassifyAtPosition (position - 1) + | res -> res match quickParseInfo with | Some (islandColumn, qualifiers, textSpan) -> @@ -99,7 +107,7 @@ type internal FSharpQuickInfoProvider async { let! sourceText = document.GetTextAsync(cancellationToken) |> Async.AwaitTask let defines = projectInfoManager.GetCompilationDefinesForEditingDocument(document) - let classification = CommonHelpers.tryClassifyAtPosition(document.Id, sourceText, document.FilePath, defines, position, false, cancellationToken) + let classification = CommonHelpers.tryClassifyAtPosition(document.Id, sourceText, document.FilePath, defines, position, cancellationToken) match classification with | Some _ -> diff --git a/Structure/BlockStructureService.fs b/Structure/BlockStructureService.fs new file mode 100644 index 00000000000..8a497434802 --- /dev/null +++ b/Structure/BlockStructureService.fs @@ -0,0 +1,119 @@ +// 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 rec Microsoft.VisualStudio.FSharp.Editor + +open System +open System.Composition +open System.Collections.Concurrent +open System.Collections.Generic +open System.Collections.Immutable +open System.Threading +open System.Threading.Tasks +open System.Linq +open System.Runtime.CompilerServices +open System.Windows +open System.Windows.Controls +open System.Windows.Media + +open Microsoft.CodeAnalysis +open Microsoft.CodeAnalysis.Completion +open Microsoft.CodeAnalysis.Classification +open Microsoft.CodeAnalysis.Editor +open Microsoft.CodeAnalysis.Editor.Shared.Utilities +open Microsoft.CodeAnalysis.Formatting +open Microsoft.CodeAnalysis.Host +open Microsoft.CodeAnalysis.Host.Mef +open Microsoft.CodeAnalysis.Options +open Microsoft.CodeAnalysis.Text +open Microsoft.CodeAnalysis.Structure + +open Microsoft.VisualStudio.FSharp.LanguageService +open Microsoft.VisualStudio.Text +open Microsoft.VisualStudio.Text.Classification +open Microsoft.VisualStudio.Text.Tagging +open Microsoft.VisualStudio.Text.Formatting +open Microsoft.VisualStudio.Shell +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 Microsoft.FSharp.Compiler.SourceCodeServices.Structure +open System.Windows.Documents + +[, FSharpCommonConstants.FSharpLanguageName); Shared>] +type internal FSharpBlockStructureServiceFactory [](checkerProvider: FSharpCheckerProvider, projectInfoManager: ProjectInfoManager) = + interface ILanguageServiceFactory with + member __.CreateLanguageService(_languageServices) = + upcast FSharpBlockStructureService(checkerProvider.Checker, projectInfoManager) + +type internal FSharpBlockStructureService(checker: FSharpChecker, projectInfoManager: ProjectInfoManager) = + inherit BlockStructureService() + let scopeToBlockType = function + | Scope.Open -> BlockTypes.Imports + | Scope.Namespace + | Scope.Module -> BlockTypes.Namespace + | Scope.Record + | Scope.Tuple + | Scope.Attribute + | Scope.Interface + | Scope.TypeExtension + | Scope.UnionCase + | Scope.EnumCase + | Scope.SimpleType + | Scope.RecordDefn + | Scope.UnionDefn + | Scope.Type -> BlockTypes.Type + | Scope.Member -> BlockTypes.Member + | Scope.LetOrUse + | Scope.Match + | Scope.IfThenElse + | Scope.ThenInIfThenElse + | Scope.ElseInIfThenElse + | Scope.MatchLambda -> BlockTypes.Conditional + | Scope.CompExpr + | Scope.TryInTryWith + | Scope.WithInTryWith + | Scope.TryFinally + | Scope.TryInTryFinally + | Scope.FinallyInTryFinally + | Scope.ObjExpr + | Scope.ArrayOrList + | Scope.CompExprInternal + | Scope.Quote + | Scope.SpecialFunc + | Scope.MatchClause + | Scope.Lambda + | Scope.LetOrUseBang + | Scope.YieldOrReturn + | Scope.YieldOrReturnBang + | Scope.RecordField + | Scope.TryWith -> BlockTypes.Expression + | Scope.Do -> BlockTypes.Statement + | Scope.While + | Scope.For -> BlockTypes.Loop + | Scope.HashDirective -> BlockTypes.PreprocessorRegion + | Scope.Comment + | Scope.XmlDocComment -> BlockTypes.Comment + | _ -> BlockTypes.Nonstructural + + override __.Language = FSharpCommonConstants.FSharpLanguageName + + override __.GetBlockStructureAsync(document, cancellationToken) : Task = + async { + match projectInfoManager.TryGetOptionsForEditingDocumentOrProject(document) with + | Some options -> + let! sourceText = document.GetTextAsync(cancellationToken) |> Async.AwaitTask + let! fileParseResults = checker.ParseFileInProject(document.FilePath, sourceText.ToString(), options) + match fileParseResults.ParseTree with + | Some parsedInput -> + let ranges = Structure.getOutliningRanges (sourceText.Lines |> Seq.map (fun x -> x.ToString()) |> Seq.toArray) parsedInput + let blockSpans = + ranges + |> Seq.map (fun range -> + BlockSpan(scopeToBlockType range.Scope, true, CommonRoslynHelpers.FSharpRangeToTextSpan(sourceText, range.Range))) + return BlockStructure(blockSpans.ToImmutableArray()) + | None -> return BlockStructure(ImmutableArray<_>.Empty) + | None -> return BlockStructure(ImmutableArray<_>.Empty) + } |> CommonRoslynHelpers.StartAsyncAsTask(cancellationToken) \ No newline at end of file