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

Enhancements for Find All References & Rename #1037

Merged
merged 26 commits into from
Mar 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
57e5a07
Dispose subscription after waiting for diags
Booksbaum Oct 30, 2022
ce18a18
Return `None` if external symbol
Booksbaum Oct 31, 2022
9f454a4
Remove `--debug` flag from tests
Booksbaum Nov 2, 2022
2c4845b
Several Enhancements around "Find References"
Booksbaum Nov 2, 2022
6ea4e94
Fix: Incorrect number of References in CodeLens
Booksbaum Nov 2, 2022
1569232
Fix: RenameProvider Test
Booksbaum Nov 2, 2022
1c3cdcb
Adjust wrong xml comment
Booksbaum Nov 2, 2022
86f35e0
Format code
Booksbaum Nov 2, 2022
16a7bbf
Change `Expect.isEmpty` to `Expecto.hasLength ... 0` to print seq in …
Booksbaum Nov 2, 2022
f1f430a
Remove leading `/` from test file path
Booksbaum Nov 2, 2022
26fdd32
Don't use .Net Framework
Booksbaum Nov 3, 2022
7257b36
Fix typo
Booksbaum Feb 15, 2023
15aa2dd
Catch exceptions in `tryFindReferencesInFile`
Booksbaum Feb 15, 2023
26d0aa1
Format code
Booksbaum Feb 15, 2023
19702b1
Remove Active Pattern FullName bug example
Booksbaum Feb 17, 2023
ab55de1
Fix error from rebase
Booksbaum Feb 21, 2023
5187ac2
Fix: `tryFindReferencesInFile` lost its `Result` when logging was added
Booksbaum Feb 21, 2023
89791e5
Format code
Booksbaum Feb 21, 2023
d58163b
Fix: `tryFindReferencesInFile` returns `Error` when Exception & `erro…
Booksbaum Feb 22, 2023
d817656
Fix: Find References doesn't find references in Script Files (Adaptiv…
Booksbaum Feb 22, 2023
9785c3f
Format Code
Booksbaum Feb 22, 2023
68d28aa
Fix error from rebase
Booksbaum Mar 2, 2023
dfcc3ea
In Reference Tests: Keep Scripts open to make AdaptiveLspServer pass
Booksbaum Mar 3, 2023
de6beb9
Enhancement: Find Usages in parallel
Booksbaum Mar 3, 2023
2f3fc62
Format code
Booksbaum Mar 3, 2023
7489c20
Change Dict to ConcurrentDict
Booksbaum Mar 3, 2023
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
500 changes: 310 additions & 190 deletions src/FsAutoComplete.Core/Commands.fs

Large diffs are not rendered by default.

183 changes: 183 additions & 0 deletions src/FsAutoComplete.Core/FileSystem.fs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ module PositionExtensions =
member x.IncColumn() = Position.mkPos x.Line (x.Column + 1)
member x.IncColumn n = Position.mkPos x.Line (x.Column + n)

member inline p.WithColumn(col) = Position.mkPos p.Line col

let inline (|Pos|) (p: FSharp.Compiler.Text.Position) = p.Line, p.Column

Expand Down Expand Up @@ -59,6 +60,10 @@ module RangeExtensions =
/// TODO: should we enforce this/use the Path members for normalization?
member x.TaggedFileName: string<LocalPath> = UMX.tag x.FileName

member inline r.With(start, fin) = Range.mkRange r.FileName start fin
member inline r.WithStart(start) = Range.mkRange r.FileName start r.End
member inline r.WithEnd(fin) = Range.mkRange r.FileName r.Start fin

/// A copy of the StringText type from F#.Compiler.Text, which is private.
/// Adds a UOM-typed filename to make range manipulation easier, as well as
/// safer traversals
Expand Down Expand Up @@ -520,3 +525,181 @@ type FileSystem(actualFs: IFileSystem, tryFindFile: string<LocalPath> -> Volatil
actualFs.OpenFileForWriteShim(filePath, ?fileMode = fileMode, ?fileAccess = fileAccess, ?fileShare = fileShare)

member _.AssemblyLoader = actualFs.AssemblyLoader

module Symbol =
open FSharp.Compiler.Symbols

/// Declaration, Implementation, Signature
let getDeclarationLocations (symbol: FSharpSymbol) =
[| symbol.DeclarationLocation
symbol.ImplementationLocation
symbol.SignatureLocation |]
|> Array.choose id
|> Array.distinct
|> Array.map (fun r -> r.NormalizeDriveLetterCasing())

/// `true` if `range` is inside at least one `declLocation`
///
/// inside instead of equal: `declLocation` for Active Pattern Case is complete Active Pattern
/// (`Even` -> declLoc: `|Even|Odd|`)
let isDeclaration (declLocations: Range[]) (range: Range) =
declLocations |> Array.exists (fun l -> Range.rangeContainsRange l range)

/// For multiple `isDeclaration` calls:
/// caches declaration locations (-> `getDeclarationLocations`) for multiple `isDeclaration` checks of same symbol
let getIsDeclaration (symbol: FSharpSymbol) =
let declLocs = getDeclarationLocations symbol
isDeclaration declLocs

/// returns `(declarations, usages)`
let partitionIntoDeclarationsAndUsages (symbol: FSharpSymbol) (ranges: Range[]) =
let isDeclaration = getIsDeclaration symbol
ranges |> Array.partition isDeclaration

module Tokenizer =
/// Extracts identifier by either looking at backticks or splitting at last `.`.
/// Removes leading paren too (from operator with Module name: `MyModule.(+++`)
///
/// Note: doesn't handle operators containing `.`,
/// but does handle strange Active Patterns (like with linebreak)
///
///
/// based on: `dotnet/fsharp` `Tokenizer.fixupSpan`
let private tryFixupRangeBySplittingAtDot (range: Range, text: NamedText, includeBackticks: bool) : Range voption =
match text[range] with
| Error _ -> ValueNone
| Ok rangeText when rangeText.EndsWith "``" ->
// find matching opening backticks

// backticks cannot contain linebreaks -- even for Active Pattern:
// `(``|Even|Odd|``)` is ok, but ` (``|Even|\n Odd|``) is not

let pre = rangeText.AsSpan(0, rangeText.Length - 2 (*backticks*) )

match pre.LastIndexOf("``") with
| -1 ->
// invalid identifier -> should not happen
range |> ValueSome
| i when includeBackticks ->
let startCol = range.EndColumn - 2 (*backticks*) - (pre.Length - i)
range.WithStart(range.End.WithColumn(startCol)) |> ValueSome
| i ->
let startCol =
range.EndColumn - 2 (*backticks*) - (pre.Length - i - 2 (*backticks*) )

let endCol = range.EndColumn - 2 (*backticks*)

range.With(range.Start.WithColumn(startCol), range.End.WithColumn(endCol))
|> ValueSome
| Ok rangeText ->
// split at `.`
// identifier (after `.`) might contain linebreak -> multiple lines
// Note: Active Pattern cannot contain `.` -> split at `.` should be always valid because we handled backticks above
// (`(|``Hello.world``|Odd|)` is not valid (neither is a type name with `.`: `type ``Hello.World`` = ...`))
match rangeText.LastIndexOf '.' with
| -1 -> range |> ValueSome
| i ->
// there might be a `(` after `.`:
// `MyModule.(+++` (Note: closing paren in not part of FSharpSymbolUse.Range)
// and there might be additional newlines and spaces afterwards
let ident = rangeText.AsSpan(i + 1 (*.*) )
let trimmedIdent = ident.TrimStart('(').TrimStart("\n\r ")
let inFrontOfIdent = ident.Length - trimmedIdent.Length

let pre = rangeText.AsSpan(0, i + 1 (*.*) + inFrontOfIdent)
// extract lines and columns
let nLines = pre.CountLines()
let lastLine = pre.LastLine()
let startLine = range.StartLine + (nLines - 1)

let startCol =
match nLines with
| 1 -> range.StartColumn + lastLine.Length
| _ -> lastLine.Length

range.WithStart(Position.mkPos startLine startCol) |> ValueSome

/// Cleans `FSharpSymbolUse.Range` (and similar) to only contain main (= last) identifier
/// * Removes leading Namespace, Module, Type: `System.String.IsNullOrEmpty` -> `IsNullOrEmpty`
/// * Removes leftover open paren: `Microsoft.FSharp.Core.Operators.(+` -> `+`
/// * keeps backticks based on `includeBackticks`
/// -> full identifier range with backticks, just identifier name (~`symbolNameCore`) without backticks
///
/// returns `None` iff `range` isn't inside `text` -> `range` & `text` for different states
let tryFixupRange (symbolNameCore: string, range: Range, text: NamedText, includeBackticks: bool) : Range voption =
// first: try match symbolNameCore in last line
// usually identifier cannot contain linebreak -> is in last line of range
// Exception: Active Pattern can span multiple lines: `(|Even|Odd|)` -> `(|Even|\n Odd|)` is valid too

/// Range in last line with actual content (-> without indentation)
let contentRangeInLastLine (range: range, lastLineText: string) =
if range.StartLine = range.EndLine then
range
else
let text = lastLineText.AsSpan(0, range.EndColumn)
// remove leading indentation
let l = text.TrimStart(' ').Length
let startCol = (range.EndColumn - l)
range.WithStart(range.End.WithColumn(startCol))

match text.GetLine range.End with
| None -> ValueNone
| Some line ->
let contentRange = contentRangeInLastLine (range, line)
assert (contentRange.StartLine = contentRange.EndLine)

let content =
line.AsSpan(contentRange.StartColumn, contentRange.EndColumn - contentRange.StartColumn)

match content.LastIndexOf symbolNameCore with
| -1 ->
// cases this can happens:
// * Active Pattern with linebreak: `(|Even|\n Odd|)`
// -> spans multiple lines
// * Active Pattern with backticks in case: `(|``Even``|Odd|)`
// -> symbolNameCore doesn't match content

// fall back to split at `.`

// differences between `tryFixupRangeBySplittingAtDot` and current function (in other match clause)
// * `tryFixupRangeBySplittingAtDot`:
// * handles strange forms of Active Patterns (like linebreak)
// * handles empty symbolName of Active Patterns Case (in decl)
// * (allocates new string)
// * current function:
// * handles operators containing `.`
// * (uses Span)

tryFixupRangeBySplittingAtDot (range, text, includeBackticks)
// Extra Pattern: `| -1 | _ when symbolNameCore = "" -> ...` is incorrect -> `when` clause applies to both...
| _ when symbolNameCore = "" ->
// happens for:
// * Active Pattern case inside Active Pattern declaration
// ```fsharp
// let (|Even|Odd|) v =
// if v % 2 = 0 then Even else Odd
// ^^^^
// ```
// -> `FSharpSymbolUse.Symbol.DisplayName` on marked position is empty
tryFixupRangeBySplittingAtDot (range, text, includeBackticks)
| i ->
let startCol = contentRange.StartColumn + i
let endCol = startCol + symbolNameCore.Length

if
includeBackticks
&&
// detect possible backticks around [startCol:endCol]
(contentRange.StartColumn <= startCol - 2 (*backticks*)
&& endCol + 2 (*backticks*) <= contentRange.EndColumn
&& (let maybeBackticks = content.Slice(i - 2, 2 + symbolNameCore.Length + 2)
maybeBackticks.StartsWith("``") && maybeBackticks.EndsWith("``")))
then
contentRange.With(
contentRange.Start.WithColumn(startCol - 2 (*backticks*) ),
contentRange.End.WithColumn(endCol + 2 (*backticks*) )
)
|> ValueSome
else
contentRange.With(contentRange.Start.WithColumn(startCol), contentRange.End.WithColumn(endCol))
|> ValueSome
24 changes: 15 additions & 9 deletions src/FsAutoComplete.Core/SymbolLocation.fs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,12 @@ let getDeclarationLocation
getDependentProjectsOfProjects
// state: State
) : SymbolDeclarationLocation option =
if symbolUse.IsPrivateToFile then

// `symbolUse.IsPrivateToFile` throws exception when no `DeclarationLocation`
if
symbolUse.Symbol.DeclarationLocation |> Option.isSome
&& symbolUse.IsPrivateToFile
then
Some SymbolDeclarationLocation.CurrentDocument
else
let isSymbolLocalForProject = symbolUse.Symbol.IsInternalToProject
Expand Down Expand Up @@ -51,13 +56,14 @@ let getDeclarationLocation
getProjectOptions (taggedFilePath)
|> Option.map (fun p -> SymbolDeclarationLocation.Projects([ p ], isSymbolLocalForProject))
else
let projectsThatContainFile = projectsThatContainFile (taggedFilePath)

let projectsThatDependOnContainingProjects =
getDependentProjectsOfProjects projectsThatContainFile
match projectsThatContainFile (taggedFilePath) with
| [] -> None
| projectsThatContainFile ->
let projectsThatDependOnContainingProjects =
getDependentProjectsOfProjects projectsThatContainFile

match projectsThatDependOnContainingProjects with
| [] -> Some(SymbolDeclarationLocation.Projects(projectsThatContainFile, isSymbolLocalForProject))
| projects ->
Some(SymbolDeclarationLocation.Projects(projectsThatContainFile @ projects, isSymbolLocalForProject))
match projectsThatDependOnContainingProjects with
| [] -> Some(SymbolDeclarationLocation.Projects(projectsThatContainFile, isSymbolLocalForProject))
| projects ->
Some(SymbolDeclarationLocation.Projects(projectsThatContainFile @ projects, isSymbolLocalForProject))
| None -> None
27 changes: 27 additions & 0 deletions src/FsAutoComplete.Core/Utils.fs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ open System
open FSharp.Compiler.CodeAnalysis
open FSharp.UMX
open FSharp.Compiler.Symbols
open System.Runtime.CompilerServices


/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
Expand Down Expand Up @@ -181,6 +182,11 @@ module Result =
| Some x -> Ok x
| None -> Error(recover ())

let inline ofVOption recover o =
match o with
| ValueSome x -> Ok x
| ValueNone -> Error(recover ())

/// ensure the condition is true before continuing
let inline guard condition errorValue =
if condition () then Ok() else Error errorValue
Expand Down Expand Up @@ -589,6 +595,27 @@ module String =
| -1 -> NoMatch
| n -> Split(s.[0 .. n - 1], s.Substring(n + 1))

[<Extension>]
type ReadOnlySpanExtensions =
/// Note: empty string -> 1 line
[<Extension>]
static member CountLines(text: ReadOnlySpan<char>) =
let mutable count = 0

for _ in text.EnumerateLines() do
count <- count + 1

count

[<Extension>]
static member LastLine(text: ReadOnlySpan<char>) =
let mutable line = ReadOnlySpan.Empty

for current in text.EnumerateLines() do
line <- current

line

type ConcurrentDictionary<'key, 'value> with

member x.TryFind key =
Expand Down
Loading