Skip to content

Commit

Permalink
Enhancements for Find All References & Rename (#1037)
Browse files Browse the repository at this point in the history
Co-authored-by: Jimmy Byrd <[email protected]>
  • Loading branch information
Booksbaum and TheAngryByrd authored Mar 3, 2023
1 parent 8ec18cc commit 0336e6e
Show file tree
Hide file tree
Showing 24 changed files with 2,609 additions and 729 deletions.
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

0 comments on commit 0336e6e

Please sign in to comment.