Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
16 changes: 15 additions & 1 deletion .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -242,4 +242,18 @@ This project uses **Paket** for dependency management instead of NuGet directly:
### Related Tools
- [FSharpLint](https://github.com/fsprojects/FSharpLint/) - Static analysis tool
- [Paket](https://fsprojects.github.io/Paket/) - Dependency management
- [FAKE](https://fake.build/) - Build automation (used for scaffolding)
- [FAKE](https://fake.build/) - Build automation (used for scaffolding)


### MCP Tools

> [!IMPORTANT]

You have access to a long-term memory system via the Model Context Protocol (MCP) at the endpoint `memorizer`. Use the following tools:
- `store`: Store a new memory. Parameters: `type`, `content` (markdown), `source`, `tags`, `confidence`, `relatedTo` (optional, memory ID), `relationshipType` (optional).
- `search`: Search for similar memories. Parameters: `query`, `limit`, `minSimilarity`, `filterTags`.
- `get`: Retrieve a memory by ID. Parameter: `id`.
- `getMany`: Retrieve multiple memories by their IDs. Parameter: `ids` (list of IDs).
- `delete`: Delete a memory by ID. Parameter: `id`.
- `createRelationship`: Create a relationship between two memories. Parameters: `fromId`, `toId`, `type`.
Use these tools to remember, recall, relate, and manage information as needed to assist the user. You can also manually retrieve or relate memories by their IDs when necessary.
8 changes: 8 additions & 0 deletions .vscode/mcp.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"servers": {
"ionide-memorizer": {
"url": "http://localhost:5001/sse",
}
},
"inputs": []
}
3 changes: 2 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ This project uses **Paket** for dependency management instead of NuGet directly:
4. Include edge cases and error conditions
5. For code fixes: Run focused tests with `dotnet run -f net8.0 --project ./test/FsAutoComplete.Tests.Lsp/FsAutoComplete.Tests.Lsp.fsproj`
6. Remove focused test markers before submitting PRs (they cause CI failures)
7. Do not delete tests without permission.

### Test Data
- Sample F# projects in `TestCases/` directories
Expand Down Expand Up @@ -242,4 +243,4 @@ This project uses **Paket** for dependency management instead of NuGet directly:
### Related Tools
- [FSharpLint](https://github.com/fsprojects/FSharpLint/) - Static analysis tool
- [Paket](https://fsprojects.github.io/Paket/) - Dependency management
- [FAKE](https://fake.build/) - Build automation (used for scaffolding)
- [FAKE](https://fake.build/) - Build automation (used for scaffolding)
143 changes: 138 additions & 5 deletions src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs
Original file line number Diff line number Diff line change
Expand Up @@ -2182,7 +2182,145 @@ type AdaptiveFSharpLspServer

}

override x.CallHierarchyOutgoingCalls(p: CallHierarchyOutgoingCallsParams) =
asyncResult {
// OutgoingCalls finds all functions/methods called FROM the current symbol
let tags = [ "CallHierarchyOutgoingCalls", box p ]
use trace = fsacActivitySource.StartActivityForType(thisType, tags = tags)

try
logger.info (
Log.setMessage "CallHierarchyOutgoingCalls Request: {params}"
>> Log.addContextDestructured "params" p
)

let filePath = Path.FileUriToLocalPath p.Item.Uri |> Utils.normalizePath
let pos = protocolPosToPos p.Item.SelectionRange.Start
let! tyRes = state.GetTypeCheckResultsForFile filePath |> AsyncResult.ofStringErr
let! parseResults = state.GetParseResults filePath |> AsyncResult.ofStringErr

// Find the binding that contains our position
let containingBinding =
(pos, parseResults.ParseTree)
||> ParsedInput.tryPickLast (fun _path node ->
match node with
| SyntaxNode.SynBinding(SynBinding(headPat = _pat; expr = expr) as binding) when
Range.rangeContainsPos binding.RangeOfBindingWithRhs pos
->
Some(binding.RangeOfBindingWithRhs, expr)
| _ -> None)

match containingBinding with
| None -> return Some [||]
| Some(bindingRange, _bodyExpr) ->

// Get all symbol uses in the entire file
let allSymbolUses = tyRes.GetCheckResults.GetAllUsesOfAllSymbolsInFile()

// Filter to symbol uses within the function body, focusing only on calls
let bodySymbolUses =
allSymbolUses
|> Seq.filter (fun su ->
Range.rangeContainsRange bindingRange su.Range
&& not su.IsFromDefinition
&& su.Range.Start <> pos
// Filter to only include actual function/method calls, not parameter references or type annotations
&& match su.Symbol with
| :? FSharpMemberOrFunctionOrValue as mfv ->
// Include functions, methods, constructors but be careful with parameters vs calls
mfv.IsFunction
|| mfv.IsMethod
|| mfv.IsConstructor
|| (mfv.IsProperty
&& not (mfv.LogicalName.Contains("get_") || mfv.LogicalName.Contains("set_")))
| :? FSharpEntity as ent ->
// Include entities only if used as constructors (when they appear in expressions)
ent.IsClass || ent.IsFSharpRecord || ent.IsFSharpUnion
| _ -> false)
|> Seq.toArray

// Group symbol uses by the called symbol
let groupedBySymbol = bodySymbolUses |> Array.groupBy (fun su -> su.Symbol.FullName)

let createOutgoingCallItem (_symbolName: string, uses: FSharp.Compiler.CodeAnalysis.FSharpSymbolUse[]) =
asyncOption {
if uses.Length = 0 then
do! None

let representativeUse = uses.[0]
let symbol = representativeUse.Symbol

// Convert the ranges where this symbol is called
let fromRanges = uses |> Array.map (fun u -> fcsRangeToLsp u.Range)

// Determine the target file and location
let! targetLocation =
match symbol.DeclarationLocation with
| Some declLoc ->
let targetFile = declLoc.FileName |> Utils.normalizePath
let targetUri = Path.LocalPathToUri targetFile

// Get symbol kind
let symbolKind =
match symbol with
| :? FSharpMemberOrFunctionOrValue as mfv ->
if mfv.IsConstructor then SymbolKind.Constructor
elif mfv.IsProperty then SymbolKind.Property
elif mfv.IsMethod then SymbolKind.Method
else SymbolKind.Function
| :? FSharpEntity as ent ->
if ent.IsClass then SymbolKind.Class
elif ent.IsInterface then SymbolKind.Interface
elif ent.IsFSharpModule then SymbolKind.Module
else SymbolKind.Object
| _ -> SymbolKind.Function

let displayName = symbol.DisplayName
let detail = $"In {System.IO.Path.GetFileName(UMX.untag targetFile)}"

Some
{ CallHierarchyItem.Name = displayName
Kind = symbolKind
Tags = None
Detail = Some detail
Uri = targetUri
Range = fcsRangeToLsp declLoc
SelectionRange = fcsRangeToLsp declLoc
Data = None }
| None ->
// Symbol without declaration location (e.g., built-in functions)
Some
{ CallHierarchyItem.Name = symbol.DisplayName
Kind = SymbolKind.Function
Tags = None
Detail = Some "Built-in"
Uri = p.Item.Uri // Use current file as fallback
Range = p.Item.Range
SelectionRange = p.Item.SelectionRange
Data = None }

return
{ CallHierarchyOutgoingCall.To = targetLocation
FromRanges = fromRanges }
}

let! outgoingCalls =
groupedBySymbol
|> Array.map createOutgoingCallItem
|> Async.parallel75
|> Async.map (Array.choose id)

return Some outgoingCalls

with e ->
trace |> Tracing.recordException e

let logCfg =
Log.setMessage "CallHierarchyOutgoingCalls Request Errored {p}"
>> Log.addContextDestructured "p" p

return! returnException e logCfg
}

override x.TextDocumentPrepareCallHierarchy(p: CallHierarchyPrepareParams) =
asyncResult {
Expand Down Expand Up @@ -3129,11 +3267,6 @@ type AdaptiveFSharpLspServer
return ()
}

member this.CallHierarchyOutgoingCalls
(_arg1: CallHierarchyOutgoingCallsParams)
: AsyncLspResult<CallHierarchyOutgoingCall array option> =
AsyncLspResult.notImplemented

member this.CancelRequest(_arg1: CancelParams) : Async<unit> = ignoreNotification
member this.NotebookDocumentDidChange(_arg1: DidChangeNotebookDocumentParams) : Async<unit> = ignoreNotification
member this.NotebookDocumentDidClose(_arg1: DidCloseNotebookDocumentParams) : Async<unit> = ignoreNotification
Expand Down
Loading