Skip to content
Open
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
1 change: 1 addition & 0 deletions docs/release-notes/.VisualStudio/18.vNext.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

* Code-fixes for FS3888 (compiler-semantic attribute on the `.fs` but not the `.fsi`): copy the attribute into the `.fsi`, or remove it from the `.fs`. ([Issue #19560](https://github.com/dotnet/fsharp/issues/19560), [PR #19880](https://github.com/dotnet/fsharp/pull/19880))
* Expand `<inheritdoc/>` in IDE tooltips, completion, and signature help, inheriting XML documentation from base classes, interfaces, overridden members, and constructors. ([Issue #19175](https://github.com/dotnet/fsharp/issues/19175), [PR #19188](https://github.com/dotnet/fsharp/pull/19188))
* F# types, modules, members and values now appear in the GitHub Copilot Chat `#` mention picker, and attach their declaration source as context. ([PR #20409](https://github.com/dotnet/fsharp/pull/20409))

### Fixed

Expand Down
3 changes: 3 additions & 0 deletions eng/Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@
ComponentModelHost would otherwise stay at 17.x; that 17.x/18.x split makes S/IComponentModel ambiguous
(CS0433). Pin to the SDK 18.9.496 version so those types resolve to a single assembly. -->
<PackageVersion Include="Microsoft.VisualStudio.ComponentModelHost" Version="18.9.453" />
<!-- Copilot chat contracts. Kept on the 18.9.x wave so its MessagePack and editor dependencies match the pins
above. Compile-only: at runtime the VS-installed contract assembly wins through Copilot's binding redirect. -->
<PackageVersion Include="Microsoft.VisualStudio.Copilot" Version="18.9.918" />
<PackageVersion Include="Microsoft.VisualStudio.Designer.Interfaces" Version="18.9.438" />
<PackageVersion Include="Microsoft.VisualStudio.Editor" Version="$(VisualStudioEditorPackagesVersion)" />
<PackageVersion Include="Microsoft.VisualStudio.Extensibility.Build" Version="17.13.39620" />
Expand Down
5 changes: 5 additions & 0 deletions vsintegration/src/FSharp.Editor/Common/Constants.fs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@ module internal FSharpConstants =
/// "F# Language Service"
let FSharpLanguageServiceCallbackName = "F# Language Service"

[<Literal>]
/// Brokered service offering F# declarations to the Copilot chat "#" mention picker.
let copilotSymbolProviderName =
"Microsoft.VisualStudio.FSharp.CopilotSymbolContextProvider"

[<Literal>]
/// "FSharp"
let FSharpLanguageLongName = "FSharp"
Expand Down
348 changes: 348 additions & 0 deletions vsintegration/src/FSharp.Editor/Copilot/CopilotContextProvider.fs

Large diffs are not rendered by default.

74 changes: 74 additions & 0 deletions vsintegration/src/FSharp.Editor/Copilot/CopilotSymbolMapping.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information.

/// Translates F# navigable items into the shapes the Copilot chat "#" mention picker understands.
module internal Microsoft.VisualStudio.FSharp.Editor.CopilotSymbolMapping

open System

open Microsoft.VisualStudio.Copilot
open Microsoft.VisualStudio.Imaging

open FSharp.Compiler.EditorServices

/// Name of the context member. It becomes the mention prefix the user sees and re-types,
/// as in "#fsharpSymbol:Namespace.Type.Member".
[<Literal>]
let SymbolMember = "fsharpSymbol"

[<Literal>]
let FullyQualifiedNameInput = "fullyQualifiedName"

/// The parse tree cannot tell an interface, struct or record apart from a plain class, so every
/// type-like declaration is reported as a class.
let symbolContextType kind =
match kind with
| NavigableItemKind.Module
| NavigableItemKind.ModuleAbbreviation
| NavigableItemKind.Exception
| NavigableItemKind.Type -> CopilotSymbolContextType.Class
| NavigableItemKind.ModuleValue -> CopilotSymbolContextType.Function
| NavigableItemKind.Field
| NavigableItemKind.Property -> CopilotSymbolContextType.Field
| NavigableItemKind.Constructor
| NavigableItemKind.Member -> CopilotSymbolContextType.Method
| NavigableItemKind.EnumCase -> CopilotSymbolContextType.Constant
| NavigableItemKind.UnionCase -> CopilotSymbolContextType.Union

let private imageId kind =
match kind with
| NavigableItemKind.Module
| NavigableItemKind.ModuleAbbreviation -> KnownImageIds.ModulePublic
| NavigableItemKind.Exception -> KnownImageIds.ExceptionPublic
| NavigableItemKind.Type -> KnownImageIds.ClassPublic
| NavigableItemKind.ModuleValue
| NavigableItemKind.Constructor
| NavigableItemKind.Member -> KnownImageIds.MethodPublic
| NavigableItemKind.Field -> KnownImageIds.FieldPublic
| NavigableItemKind.Property -> KnownImageIds.PropertyPublic
| NavigableItemKind.EnumCase
| NavigableItemKind.UnionCase -> KnownImageIds.EnumerationItemPublic

let icon kind =
CopilotImageMoniker(Guid = KnownImageIds.ImageCatalogGuid, Id = imageId kind)

/// Dotted path that both drives the picker's pattern matching and identifies a picked mention
/// when it is resolved back to source.
let fullyQualifiedName (item: NavigableItem) =
match item.Container.FullName with
| "" -> item.Name
| container -> $"{container}.{item.Name}"

/// Answers what comparing against `fullyQualifiedName` would, without building the dotted path -
/// a solution-wide scan asks this of every declaration it walks past.
let hasFullyQualifiedName (candidate: string) (item: NavigableItem) =
let candidate = candidate.AsSpan()
let container = item.Container.FullName
let name = item.Name.AsSpan()

if container.Length = 0 then
candidate.Equals(name, StringComparison.Ordinal)
else
candidate.Length = container.Length + 1 + name.Length
&& candidate[container.Length] = '.'
&& candidate.Slice(0, container.Length).Equals(container.AsSpan(), StringComparison.Ordinal)
&& candidate.Slice(container.Length + 1).Equals(name, StringComparison.Ordinal)
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information.

/// Widens the identifier range of a navigable item to the declaration a reader would recognise.
module internal Microsoft.VisualStudio.FSharp.Editor.CopilotSymbolSnippets

open System

open FSharp.Compiler.EditorServices

/// A module scope can span a whole file, which is more than a chat prompt can usefully carry.
[<Literal>]
let MaxSnippetLines = 200

/// Inclusive, 1-based line bounds of the declaration `item` names, including its doc comment.
let definitionLines (sourceLines: string array) (scopes: Structure.ScopeRange seq) (item: NavigableItem) =
let declarationLine = item.Range.StartLine

// A construct's outlining range reaches back over the doc comment in front of it, so it is the
// collapse range - the body proper - that tells which construct is declared on this line.
let declaredHere (scope: Structure.ScopeRange) =
scope.CollapseRange.StartLine = declarationLine
&& scope.Range.EndLine >= item.Range.EndLine
&& scope.Scope <> Structure.Scope.Comment
&& scope.Scope <> Structure.Scope.XmlDocComment

let mutable widest = ValueNone

for scope in scopes do
if declaredHere scope then
match widest with
| ValueSome(previous: Structure.ScopeRange) when previous.Range.EndLine >= scope.Range.EndLine -> ()
| _ -> widest <- ValueSome scope

// A one-line member declares no scope of its own; it stands for itself rather than for the type around it.
let firstLine, lastLine =
match widest with
| ValueSome scope -> scope.Range.StartLine, scope.Range.EndLine
| ValueNone -> declarationLine, item.Range.EndLine

// Outlining reports a doc comment only once it spans several lines, so a one-line "///" in front of
// a declaration is invisible to the scopes above.
let isDocComment line =
sourceLines[line - 1].AsSpan().TrimStart().StartsWith("///".AsSpan(), StringComparison.Ordinal)

let rec docCommentStart line =
if line > 1 && isDocComment (line - 1) then
docCommentStart (line - 1)
else
line

let firstLine = docCommentStart firstLine

struct (firstLine, min lastLine (firstLine + MaxSnippetLines - 1))
4 changes: 4 additions & 0 deletions vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,9 @@
<Compile Include="Navigation\NavigateToSearchService.fs" />
<Compile Include="Navigation\FindUsagesService.fs" />
<Compile Include="Navigation\FindDefinitionService.fs" />
<Compile Include="Copilot\CopilotSymbolMapping.fs" />
<Compile Include="Copilot\CopilotSymbolSnippets.fs" />
<Compile Include="Copilot\CopilotContextProvider.fs" />
<Compile Include="QuickInfo\WpfFactories.fs" />
<Compile Include="QuickInfo\Views.fs" />
<Compile Include="QuickInfo\QuickInfoProvider.fs" />
Expand Down Expand Up @@ -179,6 +182,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.EditorFeatures.Text" PrivateAssets="all" ExcludeAssets="runtime;contentFiles;build;analyzers;native" />
<PackageReference Include="Microsoft.CodeAnalysis.EditorFeatures" PrivateAssets="all" ExcludeAssets="runtime;contentFiles;build;analyzers;native" />
<PackageReference Include="Microsoft.VisualStudio.Copilot" PrivateAssets="all" ExcludeAssets="runtime;contentFiles;build;analyzers;native" />
<PackageReference Include="Microsoft.VisualStudio.LanguageServices.ExternalAccess" PrivateAssets="all" ExcludeAssets="runtime;contentFiles;build;analyzers;native" />
<PackageReference Include="Microsoft.VisualStudio.ProjectSystem" PrivateAssets="all" ExcludeAssets="runtime;contentFiles;build;analyzers;native" />
<PackageReference Include="Microsoft.VisualStudio.ProjectSystem.Managed" PrivateAssets="all" ExcludeAssets="runtime;contentFiles;build;analyzers;native" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,27 +6,31 @@ open System
open System.ComponentModel.Design
open System.Runtime.InteropServices
open System.Threading
open System.Threading.Tasks
open System.IO
open System.Collections.Immutable
open Microsoft.CodeAnalysis
open Microsoft.CodeAnalysis.ExternalAccess.FSharp
open Microsoft.CodeAnalysis.Host.Mef
open Microsoft.CodeAnalysis.Options
open FSharp.Compiler
open FSharp.Compiler.CodeAnalysis
open FSharp.NativeInterop
open Microsoft.ServiceHub.Framework
open Microsoft.VisualStudio
open Microsoft.VisualStudio.Copilot
open Microsoft.VisualStudio.FSharp.Editor
open Microsoft.VisualStudio.LanguageServices
open Microsoft.VisualStudio.LanguageServices.Implementation.LanguageService
open Microsoft.VisualStudio.LanguageServices.Implementation.ProjectSystem
open Microsoft.VisualStudio.Shell
open Microsoft.VisualStudio.Shell.Interop
open Microsoft.VisualStudio.Shell.ServiceBroker
open Microsoft.VisualStudio.Text.Outlining
open Microsoft.CodeAnalysis.ExternalAccess.FSharp
open Microsoft.CodeAnalysis.Host.Mef
open Microsoft.VisualStudio.Editor
open Microsoft.VisualStudio.FSharp.Editor.Telemetry
open CancellableTasks
open FSharp.Compiler
open FSharp.Compiler.CodeAnalysis
open FSharp.NativeInterop
open FSharp.Compiler.Text
open Microsoft.VisualStudio.Editor
open CancellableTasks

#nowarn "9" // NativePtr.toNativeInt
#nowarn "57" // Experimental stuff
Expand Down Expand Up @@ -408,8 +412,47 @@ type internal FSharpPackage() as this =
|> CancellableTask.startAsTask cancellationToken)
)

override this.RegisterOnAfterPackageLoadedAsyncWork(afterPackageLoadedTasks: PackageLoadTasks) =
base.RegisterOnAfterPackageLoadedAsyncWork(afterPackageLoadedTasks)

afterPackageLoadedTasks.AddTask(
false,
fun _ cancellationToken ->
task {
try
let! container = this.GetServiceAsync(typeof<SVsBrokeredServiceContainer>)

match container with
| :? IBrokeredServiceContainer as container ->
// The Interactions service also serves the registration interface. It is absent when
// GitHub Copilot is not installed, in which case the proxy is null and F# stays out of the picker.
let! registration =
container
.GetFullAccessServiceBroker()
.GetProxyAsync<ICopilotRegistrationService>(CopilotDescriptors.InteractionService, cancellationToken)

use registration = registration

match registration with
| null -> ()
| registration ->
let moniker =
ServiceMoniker(
FSharpConstants.copilotSymbolProviderName,
Version CopilotDescriptors.CurrentContextProviderVersion
)

do! registration.RegisterContextProviderAsync(moniker, cancellationToken)
| _ -> ()
// Package load runs its tasks back to back on one loop, so a Copilot failure - a contract
// version the installed build does not serve, say - must not take the F# package down with it.
with ex when not (ex :? OperationCanceledException) ->
DebugHelpers.FSharpOutputPane.logExceptionWithContext (ex, "Registering the Copilot context provider")
}
:> Task
)

#if DEBUG
override _.RegisterOnAfterPackageLoadedAsyncWork(afterPackageLoadedTasks: PackageLoadTasks) =
afterPackageLoadedTasks.AddTask(
false,
fun _ _ ->
Expand Down
Loading
Loading