Skip to content

Commit

Permalink
Detect type alias in implementation. (#1243)
Browse files Browse the repository at this point in the history
* Detect type alias in implementation.

* Check if is file pair.

* Initial insert in signature file.

* Add first unit test

* Format code

* Clean up
  • Loading branch information
nojaf committed Mar 14, 2024
1 parent 4343ccc commit 8162411
Show file tree
Hide file tree
Showing 11 changed files with 449 additions and 42 deletions.
2 changes: 1 addition & 1 deletion .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,4 @@ indent_size = 2
fsharp_max_array_or_list_width=80
fsharp_max_dot_get_expression_width=80
fsharp_max_function_binding_width=80
fsharp_max_value_binding_width=80
fsharp_max_value_binding_width=80
4 changes: 4 additions & 0 deletions src/FsAutoComplete.Core/ParseAndCheckResults.fs
Original file line number Diff line number Diff line change
Expand Up @@ -519,6 +519,10 @@ type ParseAndCheckResults
let identIsland = Array.toList identIsland
checkResults.GetSymbolUseAtLocation(pos.Line, colu, lineStr, identIsland)

member x.TryGetSymbolUseFromIdent (sourceText: ISourceText) (ident: Ident) : FSharpSymbolUse option =
let line = sourceText.GetLineString(ident.idRange.EndLine - 1)
x.GetCheckResults.GetSymbolUseAtLocation(ident.idRange.EndLine, ident.idRange.EndColumn, line, [ ident.idText ])

member __.TryGetSymbolUses (pos: Position) (lineStr: LineStr) : FSharpSymbolUse list =
match Lexer.findLongIdents (pos.Column, lineStr) with
| None -> []
Expand Down
2 changes: 2 additions & 0 deletions src/FsAutoComplete.Core/ParseAndCheckResults.fsi
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ type ParseAndCheckResults =

member TryGetSymbolUse: pos: Position -> lineStr: LineStr -> FSharpSymbolUse option

member TryGetSymbolUseFromIdent: ISourceText -> Ident -> FSharpSymbolUse option

member TryGetSymbolUses: pos: Position -> lineStr: LineStr -> FSharpSymbolUse list

member TryGetSymbolUseAndUsages:
Expand Down
200 changes: 200 additions & 0 deletions src/FsAutoComplete/CodeFixes/AddTypeAliasToSignatureFile.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
module FsAutoComplete.CodeFix.AddTypeAliasToSignatureFile

open System
open FSharp.Compiler.Symbols
open FSharp.Compiler.Syntax
open FSharp.Compiler.Text
open FSharp.Compiler.CodeAnalysis
open FsToolkit.ErrorHandling
open Ionide.LanguageServerProtocol.Types
open FsAutoComplete.CodeFix.Types
open FsAutoComplete
open FsAutoComplete.LspHelpers

let mkLongIdRange (lid: LongIdent) = lid |> List.map (fun ident -> ident.idRange) |> List.reduce Range.unionRanges

let (|AllOpenOrHashDirective|_|) (decls: SynModuleSigDecl list) : range option =
match decls with
| [] -> None
| decls ->

let allOpenOrHashDirective =
decls
|> List.forall (function
| SynModuleSigDecl.Open _
| SynModuleSigDecl.HashDirective _ -> true
| _ -> false)

if not allOpenOrHashDirective then
None
else
Some (List.last decls).Range.EndRange

type SynTypeDefn with

member x.FullRange =
match x with
| SynTypeDefn(range = m; trivia = { LeadingKeyword = lk }) -> Range.unionRanges lk.Range m

let title = "Add type alias to signature file"

let codeFixForImplementationFileWithSignature
(getProjectOptionsForFile: GetProjectOptionsForFile)
(codeFix: CodeFix)
(codeActionParams: CodeActionParams)
: Async<Result<Fix list, string>> =
async {
let fileName = codeActionParams.TextDocument.GetFilePath() |> Utils.normalizePath
let! project = getProjectOptionsForFile fileName

match project with
| Error _ -> return Ok []
| Ok projectOptions ->

let signatureFile = String.Concat(fileName, "i")
let hasSig = projectOptions.SourceFiles |> Array.contains signatureFile

if not hasSig then
return Ok []
else
return! codeFix codeActionParams
}

let fix
(getProjectOptionsForFile: GetProjectOptionsForFile)
(getParseResultsForFile: GetParseResultsForFile)
: CodeFix =
codeFixForImplementationFileWithSignature getProjectOptionsForFile (fun (codeActionParams: CodeActionParams) ->
asyncResult {
let fileName = codeActionParams.TextDocument.GetFilePath() |> Utils.normalizePath
// The converted LSP start position to an FCS start position.
let fcsPos = protocolPosToPos codeActionParams.Range.Start
// The syntax tree and typed tree, current line and sourceText of the current file.
let! (parseAndCheckResults: ParseAndCheckResults, _line: string, sourceText: IFSACSourceText) =
getParseResultsForFile fileName fcsPos

let typeDefnInfo =
(fcsPos, parseAndCheckResults.GetParseResults.ParseTree)
||> ParsedInput.tryPick (fun _path node ->
match node with
| SyntaxNode.SynTypeDefn(SynTypeDefn(
typeInfo = SynComponentInfo(longId = [ typeIdent ])
typeRepr = SynTypeDefnRepr.Simple(simpleRepr = SynTypeDefnSimpleRepr.TypeAbbrev _)) as tdn) when
(Range.rangeContainsPos tdn.FullRange fcsPos)
->
Some(typeIdent, tdn.FullRange)
| _ -> None)

match typeDefnInfo with
| None -> return []
| Some(typeName, mTypeDefn) ->

match parseAndCheckResults.TryGetSymbolUseFromIdent sourceText typeName with
| None -> return []
| Some typeSymbolUse ->

match typeSymbolUse.Symbol with
| :? FSharpEntity as entity ->
let isPartOfSignature =
match entity.SignatureLocation with
| None -> false
| Some sigLocation -> Utils.isSignatureFile sigLocation.FileName

if isPartOfSignature then
return []
else

let implFilePath = codeActionParams.TextDocument.GetFilePath()
let sigFilePath = $"%s{implFilePath}i"
let sigFileName = Utils.normalizePath sigFilePath

let sigTextDocumentIdentifier: TextDocumentIdentifier =
{ Uri = $"%s{codeActionParams.TextDocument.Uri}i" }

let! (sigParseAndCheckResults: ParseAndCheckResults, _sigLine: string, sigSourceText: IFSACSourceText) =
getParseResultsForFile sigFileName (Position.mkPos 1 0)

let parentSigLocation =
entity.DeclaringEntity
|> Option.bind (fun parentEntity ->
match parentEntity.SignatureLocation with
| Some sigLocation when Utils.isSignatureFile sigLocation.FileName -> Some sigLocation
| _ -> None)

match parentSigLocation with
| None -> return []
| Some parentSigLocation ->

// Find a good location to insert the type alias
let insertText =
(parentSigLocation.Start, sigParseAndCheckResults.GetParseResults.ParseTree)
||> ParsedInput.tryPick (fun _path node ->
match node with
| SyntaxNode.SynModuleOrNamespaceSig(SynModuleOrNamespaceSig(longId = longId; decls = decls))
| SyntaxNode.SynModuleSigDecl(SynModuleSigDecl.NestedModule(
moduleInfo = SynComponentInfo(longId = longId); moduleDecls = decls)) ->
let mSigName = mkLongIdRange longId

// `parentSigLocation` will only contain the single identifier in case a module is prefixed with a namespace.
if not (Range.rangeContainsRange mSigName parentSigLocation) then
None
else

let aliasText =
let text = sourceText.GetSubTextFromRange mTypeDefn

if not (text.StartsWith("and", StringComparison.Ordinal)) then
text
else
String.Concat("type", text.Substring 3)

match decls with
| [] ->
match node with
| SyntaxNode.SynModuleOrNamespaceSig nm ->
Some(nm.Range.EndRange, String.Concat("\n\n", aliasText))

| SyntaxNode.SynModuleSigDecl(SynModuleSigDecl.NestedModule(
range = mNested
trivia = { ModuleKeyword = Some mModule
EqualsRange = Some mEquals })) ->
let moduleEqualsText =
sigSourceText.GetSubTextFromRange(Range.unionRanges mModule mEquals)
// Can this grabbed from configuration?
let indent = " "

Some(mNested, String.Concat(moduleEqualsText, "\n", indent, aliasText))
| _ -> None
| AllOpenOrHashDirective mLastDecl -> Some(mLastDecl, String.Concat("\n\n", aliasText))
| decls ->

decls
// Skip open statements
|> List.tryFind (function
| SynModuleSigDecl.Open _
| SynModuleSigDecl.HashDirective _ -> false
| _ -> true)
|> Option.map (fun mdl ->
let offset =
if mdl.Range.StartColumn = 0 then
String.Empty
else
String.replicate mdl.Range.StartColumn " "

mdl.Range.StartRange, String.Concat(aliasText, "\n\n", offset))
| _ -> None)

match insertText with
| None -> return []
| Some(mInsert, newText) ->

return
[ { SourceDiagnostic = None
Title = title
File = sigTextDocumentIdentifier
Edits =
[| { Range = fcsRangeToLsp mInsert
NewText = newText } |]
Kind = FixKind.Fix } ]
| _ -> return []
})
6 changes: 6 additions & 0 deletions src/FsAutoComplete/CodeFixes/AddTypeAliasToSignatureFile.fsi
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module FsAutoComplete.CodeFix.AddTypeAliasToSignatureFile

open FsAutoComplete.CodeFix.Types

val title: string
val fix: getProjectOptionsForFile: GetProjectOptionsForFile -> getParseResultsForFile: GetParseResultsForFile -> CodeFix
3 changes: 2 additions & 1 deletion src/FsAutoComplete/LspServers/AdaptiveServerState.fs
Original file line number Diff line number Diff line change
Expand Up @@ -1902,7 +1902,8 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac
ToInterpolatedString.fix tryGetParseAndCheckResultsForFile getLanguageVersion
AdjustConstant.fix tryGetParseAndCheckResultsForFile
UpdateValueInSignatureFile.fix tryGetParseAndCheckResultsForFile
RemoveUnnecessaryParentheses.fix forceFindSourceText |])
RemoveUnnecessaryParentheses.fix forceFindSourceText
AddTypeAliasToSignatureFile.fix forceGetFSharpProjectOptions tryGetParseAndCheckResultsForFile |])

let forgetDocument (uri: DocumentUri) =
async {
Expand Down
Loading

0 comments on commit 8162411

Please sign in to comment.