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

Change format of requests to show module sources #12351

Merged
merged 8 commits into from
Nov 13, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public AndConstraint<SourceArchiveAssertions> BeEquivalentTo(SourceArchive archi

Subject.Should().NotBeNull();

Subject!.EntrypointPath.Should().Be(archive.EntrypointPath);
Subject!.EntrypointRelativePath.Should().Be(archive.EntrypointRelativePath);
Subject.SourceFiles.Select(entry => entry.Path).Should().BeEquivalentTo(archive.SourceFiles.Select(entry => entry.Path));

var ourFiles = Subject.SourceFiles.ToArray();
Expand Down
4 changes: 2 additions & 2 deletions src/Bicep.Core.UnitTests/Registry/SourceArchiveTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ public void CanPackAndUnpackSourceFiles()
stream.Length.Should().BeGreaterThan(0);

SourceArchive sourceArchive = SourceArchive.FromStream(stream);
sourceArchive.EntrypointPath.Should().Be("main.bicep");
sourceArchive.EntrypointRelativePath.Should().Be("main.bicep");


var archivedFiles = sourceArchive.SourceFiles.ToArray();
Expand Down Expand Up @@ -212,7 +212,7 @@ public void CanPackAndUnpackSourceFiles()

SourceArchive sourceArchive = SourceArchive.FromStream(stream);

sourceArchive.EntrypointPath.Should().Be("my main.bicep");
sourceArchive.EntrypointRelativePath.Should().Be("my main.bicep");

var archivedTestFile = sourceArchive.SourceFiles.Single(f => f.Path != "my main.bicep");
archivedTestFile.Path.Should().Be(expecteArchivedUri);
Expand Down
4 changes: 2 additions & 2 deletions src/Bicep.Core/SourceCode/SourceArchive.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ namespace Bicep.Core.SourceCode
public partial class SourceArchive
{
public ImmutableArray<SourceFileInfo> SourceFiles { get; init; }
public string EntrypointPath { get; init; }
public string EntrypointRelativePath { get; init; }

public const string SourceKind_Bicep = "bicep";
public const string SourceKind_ArmTemplate = "armTemplate";
Expand Down Expand Up @@ -183,7 +183,7 @@ private SourceArchive(Stream stream)
infos.Add(new SourceFileInfo(info.Path, info.ArchivePath, info.Kind, contents));
}

this.EntrypointPath = metadata.EntryPoint;
this.EntrypointRelativePath = metadata.EntryPoint;
this.SourceFiles = infos.ToImmutableArray();
}

Expand Down

Large diffs are not rendered by default.

48 changes: 12 additions & 36 deletions src/Bicep.LangServer/Handlers/BicepDefinitionHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
using Bicep.Core.Navigation;
using Bicep.Core.Parsing;
using Bicep.Core.Registry;
using Bicep.Core.Registry.Oci;
using Bicep.Core.Semantics;
using Bicep.Core.Semantics.Metadata;
using Bicep.Core.SourceCode;
Expand Down Expand Up @@ -120,7 +121,8 @@ private LocationOrLocationLinks HandleUnboundSymbolLocation(DefinitionParams req
&& matchingNodes[^3] is ModuleDeclarationSyntax moduleDeclarationSyntax
&& matchingNodes[^2] is StringSyntax stringToken
&& context.Compilation.SourceFileGrouping.TryGetSourceFile(moduleDeclarationSyntax).IsSuccess(out var sourceFile)
&& this.moduleDispatcher.TryGetArtifactReference(moduleDeclarationSyntax, request.TextDocument.Uri.ToUriEncoded()).IsSuccess(out var moduleReference))
&& this.moduleDispatcher.TryGetArtifactReference(moduleDeclarationSyntax, request.TextDocument.Uri.ToUriEncoded()).IsSuccess(out var artifactReference)
&& artifactReference is OciArtifactReference moduleReference)
{
return HandleModuleReference(context, stringToken, sourceFile, moduleReference);
}
Expand All @@ -133,7 +135,8 @@ private LocationOrLocationLinks HandleUnboundSymbolLocation(DefinitionParams req
&& matchingNodes[^4] is CompileTimeImportDeclarationSyntax importDeclarationSyntax
&& matchingNodes[^2] is StringSyntax stringToken
&& context.Compilation.SourceFileGrouping.TryGetSourceFile(importDeclarationSyntax).IsSuccess(out var sourceFile)
&& this.moduleDispatcher.TryGetArtifactReference(importDeclarationSyntax, request.TextDocument.Uri.ToUriEncoded()).IsSuccess(out var moduleReference))
&& this.moduleDispatcher.TryGetArtifactReference(importDeclarationSyntax, request.TextDocument.Uri.ToUriEncoded()).IsSuccess(out var artifactReference)
&& artifactReference is OciArtifactReference moduleReference)
{
// goto beginning of the module file.
return GetFileDefinitionLocation(
Expand Down Expand Up @@ -178,7 +181,7 @@ private LocationOrLocationLinks HandleUnboundSymbolLocation(DefinitionParams req
return new();
}

private LocationOrLocationLinks HandleModuleReference(CompilationContext context, StringSyntax stringToken, ISourceFile sourceFile, ArtifactReference reference)
private LocationOrLocationLinks HandleModuleReference(CompilationContext context, StringSyntax stringToken, ISourceFile sourceFile, OciArtifactReference reference)
{
// Return the correct link format so our language client can display the sources
return GetFileDefinitionLocation(
Expand All @@ -188,50 +191,23 @@ private LocationOrLocationLinks HandleModuleReference(CompilationContext context
new() { Start = new(0, 0), End = new(0, 0) });
}

private Uri GetModuleSourceLinkUri(ISourceFile sourceFile, ArtifactReference reference)
private Uri GetModuleSourceLinkUri(ISourceFile sourceFile, OciArtifactReference reference)
{
if (!this.CanClientAcceptRegistryContent() || !reference.IsExternal)
{
// the client doesn't support the bicep-cache scheme or we're dealing with a local module
// the client doesn't support the bicep-extsrc scheme or we're dealing with a local module
// just use the file URI
return sourceFile.FileUri;
}

// this path is specific to clients that indicate to the server that they can handle bicep-cache document URIs
// the client expectation when the user navigates to a file with a bicep-cache:https:// URI is to request file content
// via the textDocument/bicepCache LSP request implemented in the BicepRegistryCacheRequestHandler.

var sourceFilePath = sourceFile.FileUri.AbsolutePath;

if (moduleDispatcher.TryGetModuleSources(reference) is SourceArchive sourceArchive)
{
// We have Bicep source code available.
// Replace the local cached JSON name (always main.json) with the actual source entrypoint filename (e.g.
// myentrypoint.bicep) so clients know to request the bicep instead of json, and so they know to use the
// bicep language server to display the code.
// e.g. "path/main.json" -> "path/myentrypoint.bicep"
// The "path/myentrypoint.bicep" path is virtual (doesn't actually exist).
var entrypointFilename = Path.GetFileName(sourceArchive.EntrypointPath);
sourceFilePath = Path.Join(Path.GetDirectoryName(sourceFilePath), entrypointFilename);
}

// The file path and fully qualified reference may contain special characters (like :) that need to be url-encoded.
sourceFilePath = WebUtility.UrlEncode(sourceFilePath);
var fullyQualifiedReference = WebUtility.UrlEncode(reference.FullyQualifiedReference);

// Encode the source file path as a path and the fully qualified reference as a fragment.
// VsCode will pass it to our language client, which will respond by requesting the source to display via
// a textDocument/bicepCache request (see BicepCacheHandler)
// Example: bicep-cache:br:myregistry.azurecr.io/myrepo:v1#/Users/MyUserName/.bicep/br/registry.azurecr.io/myrepo/v1$/main.json (encoded)
// or if source is available:
// Example: bicep-cache:br:myregistry.azurecr.io/myrepo:v1#/Users/MyUserName/.bicep/br/registry.azurecr.io/myrepo/v1$/entrypoint.bicep (encoded)
return new Uri($"bicep-cache:{fullyQualifiedReference}#{sourceFilePath}");
return BicepExternalSourceRequestHandler.GetExternalSourceLinkUri(reference, moduleDispatcher.TryGetModuleSources(reference));
}

private LocationOrLocationLinks HandleWildcardImportDeclaration(CompilationContext context, DefinitionParams request, SymbolResolutionResult result, WildcardImportSymbol wildcardImport)
{
if (context.Compilation.SourceFileGrouping.TryGetSourceFile(wildcardImport.EnclosingDeclaration).IsSuccess(out var sourceFile) &&
wildcardImport.TryGetArtifactReference().IsSuccess(out var moduleReference))
wildcardImport.TryGetArtifactReference().IsSuccess(out var artifactReference)
&& artifactReference is OciArtifactReference moduleReference)
{
return GetFileDefinitionLocation(
GetModuleSourceLinkUri(sourceFile, moduleReference),
Expand Down Expand Up @@ -553,7 +529,7 @@ private static (Template?, Uri?) GetArmSourceTemplateInfo(CompilationContext con
_ => null,
};

// True if the client knows how (like our vscode extension) to handle the "bicep-cache:" schema
// True if the client knows how (like our vscode extension) to handle the "bicep-extsrc:" schema
private bool CanClientAcceptRegistryContent()
{
if (this.languageServer.ClientSettings.InitializationOptions is not JObject obj ||
Expand Down
140 changes: 140 additions & 0 deletions src/Bicep.LangServer/Handlers/BicepExternalSourceRequestHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

most of this file same as renamed file except name changes

using System;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Bicep.Core.Diagnostics;
using Bicep.Core.FileSystem;
using Bicep.Core.Registry;
using Bicep.Core.Registry.Oci;
using Bicep.Core.SourceCode;
using MediatR;
using OmniSharp.Extensions.JsonRpc;

namespace Bicep.LanguageServer.Handlers
{
[Method(BicepExternalSourceRequestHandler.BicepExternalSourceLspMethodName, Direction.ClientToServer)]
public record BicepExternalSourceParams(
string Target // The module reference to display sources for
) : IRequest<BicepExternalSourceResponse>;

public record BicepExternalSourceResponse(string Content);

/// <summary>
/// Handles textDocument/bicepExternalSource LSP requests. These are sent by clients that are resolving contents of document URIs using the bicep-extsrc: scheme.
/// The BicepDefinitionHandler returns such URIs when definitions are inside modules that reside in the local module cache.
/// </summary>
public class BicepExternalSourceRequestHandler : IJsonRpcRequestHandler<BicepExternalSourceParams, BicepExternalSourceResponse>
{
public const string BicepExternalSourceLspMethodName = "textDocument/bicepExternalSource";

private readonly IModuleDispatcher moduleDispatcher;
private readonly IFileResolver fileResolver;

public BicepExternalSourceRequestHandler(IModuleDispatcher moduleDispatcher, IFileResolver fileResolver)
{
this.moduleDispatcher = moduleDispatcher;
this.fileResolver = fileResolver;
}

public Task<BicepExternalSourceResponse> Handle(BicepExternalSourceParams request, CancellationToken cancellationToken)
{
// If any of the following paths result in an exception being thrown (and surfaced client-side to the user),
// it indicates a code defect client or server-side.
// In normal operation, the user should never see them regardless of how malformed their code is.

if (!moduleDispatcher.TryGetArtifactReference(ArtifactType.Module, request.Target, new Uri("file:https:///no-parent-file-is-available")).IsSuccess(out var moduleReference))
{
throw new InvalidOperationException(
$"The client specified an invalid module reference '{request.Target}'.");
}

if (!moduleReference.IsExternal)
{
throw new InvalidOperationException(
$"The specified module reference '{request.Target}' refers to a local module which is not supported by {BicepExternalSourceLspMethodName} requests.");
}

if (this.moduleDispatcher.GetArtifactRestoreStatus(moduleReference, out _) != ArtifactRestoreStatus.Succeeded)
{
throw new InvalidOperationException(
$"The module '{moduleReference.FullyQualifiedReference}' has not yet been successfully restored.");
}

if (!moduleDispatcher.TryGetLocalArtifactEntryPointUri(moduleReference).IsSuccess(out var uri))
{
throw new InvalidOperationException(
$"Unable to obtain the entry point URI for module '{moduleReference.FullyQualifiedReference}'.");
}

if (moduleDispatcher.TryGetModuleSources(moduleReference) is SourceArchive sourceArchive)
{
// TODO: For now, we just proffer the main source file
var entrypointFile = sourceArchive.SourceFiles.Single(f => f.Path == sourceArchive.EntrypointRelativePath);
return Task.FromResult(new BicepExternalSourceResponse(entrypointFile.Contents));
}

// No sources available, just retrieve the JSON source
if (!this.fileResolver.TryRead(uri).IsSuccess(out var contents, out var failureBuilder))
{
var message = failureBuilder(DiagnosticBuilder.ForDocumentStart()).Message;
throw new InvalidOperationException($"Unable to read file '{uri}'. {message}");
}

return Task.FromResult(new BicepExternalSourceResponse(contents));
}

/// <summary>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

new code

/// Creates a bicep-extsrc: URI for a given module's source file to give to the client to use as a document URI. (Client will then
/// ask for us the source code via a textDocument/externalSource request).
/// </summary>
/// <param name="reference">The module reference</param>
/// <param name="sourceArchive">The source archive for the module, if sources are available</param>
/// <returns>A bicep-extsrc: URI</returns>
public static Uri GetExternalSourceLinkUri(OciArtifactReference reference, SourceArchive? sourceArchive)
{
string entrypoint;
string? requestedSourceFile;

if (sourceArchive is { })
{
// We have Bicep source code available.
entrypoint = sourceArchive.EntrypointRelativePath;
requestedSourceFile = entrypoint;
}
else
{
// Just showing the main.json
entrypoint = "main.json";
requestedSourceFile = null;
}

var fullyQualifiedReference = reference.FullyQualifiedReference;
var version = reference.Tag is string ? $":{reference.Tag}" : $"@{reference.Digest}";

var shortDocumentTitle = $"{entrypoint} ({Path.GetFileName(reference.Repository)}{version})";
var fullDocumentTitle = $"{reference.Scheme}:{reference.Registry}/{reference.Repository}{version}/{shortDocumentTitle}";

// Encode the module reference as a query and the file to retrieve as a fragment.
// Vs Code will strip the fragment and query and use the main part of the uri as the document title.
// The Bicep extension will use the fragment to make a call to use via textDocument/bicepExternalSource request (see BicepExternalSourceHandler)
// to get the actual source code contents to display.
//
// Example:
//
// source available (will be encoded):
// bicep-extsrc:br:myregistry.azurecr.io/myrepo:main.bicep (v1)?br:myregistry.azurecr.io/myrepo:v1#main.bicep
//
// source not available, showing just JSON (will be encoded)
// bicep-extsrc:br:myregistry.azurecr.io/myrepo:main.json (v1)?br:myregistry.azurecr.io/myrepo:v1
//
var uri = new UriBuilder($"bicep-extsrc:{Uri.EscapeDataString(fullDocumentTitle)}");
uri.Query = Uri.EscapeDataString(fullyQualifiedReference);
uri.Fragment = requestedSourceFile is null ? null : Uri.EscapeDataString(requestedSourceFile);
return uri.Uri;
}
}
}
Loading
Loading