File: Workspaces\LspMiscellaneousFilesWorkspace.cs
Web Access
Project: src\src\LanguageServer\Protocol\Microsoft.CodeAnalysis.LanguageServer.Protocol.csproj (Microsoft.CodeAnalysis.LanguageServer.Protocol)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Features.Workspaces;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.MetadataAsSource;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Microsoft.CommonLanguageServerProtocol.Framework;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.LanguageServer;
 
/// <summary>
/// Defines a default workspace for opened LSP files that are not found in any
/// workspace registered by the <see cref="LspWorkspaceRegistrationService"/>.
/// If a document added here is subsequently found in a registered workspace, 
/// the document is removed from this workspace.
/// 
/// Future work for this workspace includes supporting basic metadata references (mscorlib, System dlls, etc),
/// but that is dependent on having a x-plat mechanism for retrieving those references from the framework / sdk.
/// </summary>
internal sealed class LspMiscellaneousFilesWorkspace(ILspServices lspServices, IMetadataAsSourceFileService metadataAsSourceFileService, HostServices hostServices)
    : Workspace(hostServices, WorkspaceKind.MiscellaneousFiles), ILspService, ILspWorkspace
{
    public bool SupportsMutation => true;
 
    /// <summary>
    /// Takes in a file URI and text and creates a misc project and document for the file.
    /// 
    /// Calls to this method and <see cref="TryRemoveMiscellaneousDocument(Uri, bool)"/> are made
    /// from LSP text sync request handling which do not run concurrently.
    /// </summary>
    public Document? AddMiscellaneousDocument(Uri uri, SourceText documentText, string languageId, ILspLogger logger)
    {
        var documentFilePath = ProtocolConversions.GetDocumentFilePathFromUri(uri);
 
        var container = new StaticSourceTextContainer(documentText);
        if (metadataAsSourceFileService.TryAddDocumentToWorkspace(documentFilePath, container, out var documentId))
        {
            var metadataWorkspace = metadataAsSourceFileService.TryGetWorkspace();
            Contract.ThrowIfNull(metadataWorkspace);
            var document = metadataWorkspace.CurrentSolution.GetRequiredDocument(documentId);
            return document;
        }
 
        var languageInfoProvider = lspServices.GetRequiredService<ILanguageInfoProvider>();
        if (!languageInfoProvider.TryGetLanguageInformation(uri, languageId, out var languageInformation))
        {
            // Only log here since throwing here could take down the LSP server.
            logger.LogError($"Could not find language information for {uri} with absolute path {documentFilePath}");
            return null;
        }
 
        var sourceTextLoader = new SourceTextLoader(documentText, documentFilePath);
 
        var projectInfo = MiscellaneousFileUtilities.CreateMiscellaneousProjectInfoForDocument(
            this, documentFilePath, sourceTextLoader, languageInformation, documentText.ChecksumAlgorithm, Services.SolutionServices, []);
        OnProjectAdded(projectInfo);
 
        var id = projectInfo.Documents.Single().Id;
        return CurrentSolution.GetRequiredDocument(id);
    }
 
    /// <summary>
    /// Removes a document with the matching file path from this workspace.
    /// 
    /// Calls to this method and <see cref="AddMiscellaneousDocument(Uri, SourceText, string, ILspLogger)"/> are made
    /// from LSP text sync request handling which do not run concurrently.
    /// </summary>
    public void TryRemoveMiscellaneousDocument(Uri uri, bool removeFromMetadataWorkspace)
    {
        var documentFilePath = ProtocolConversions.GetDocumentFilePathFromUri(uri);
        if (removeFromMetadataWorkspace && metadataAsSourceFileService.TryRemoveDocumentFromWorkspace(documentFilePath))
        {
            return;
        }
 
        // We'll only ever have a single document matching this URI in the misc solution.
        var matchingDocument = CurrentSolution.GetDocumentIds(uri).SingleOrDefault();
        if (matchingDocument != null)
        {
            if (CurrentSolution.ContainsDocument(matchingDocument))
            {
                OnDocumentRemoved(matchingDocument);
            }
            else if (CurrentSolution.ContainsAdditionalDocument(matchingDocument))
            {
                OnAdditionalDocumentRemoved(matchingDocument);
            }
 
            // Also remove the project - we always create a new project for each misc file we add
            // so it should never have other documents in it.
            var project = CurrentSolution.GetRequiredProject(matchingDocument.ProjectId);
            OnProjectRemoved(project.Id);
        }
    }
 
    public ValueTask UpdateTextIfPresentAsync(DocumentId documentId, SourceText sourceText, CancellationToken cancellationToken)
    {
        this.OnDocumentTextChanged(documentId, sourceText, PreservationMode.PreserveIdentity, requireDocumentPresent: false);
        return ValueTaskFactory.CompletedTask;
    }
 
    private sealed class StaticSourceTextContainer(SourceText text) : SourceTextContainer
    {
        public override SourceText CurrentText => text;
 
        /// <summary>
        /// Text changes are handled by LSP forking the document, we don't need to actually update anything here.
        /// </summary>
        public override event EventHandler<TextChangeEventArgs> TextChanged
        {
            add { }
            remove { }
        }
    }
}