// // Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. // using System; using System.Collections.Generic; using System.IO; using System.Text; using System.Text.RegularExpressions; using System.Linq; using Microsoft.SqlTools.EditorServices.Utility; using Microsoft.SqlTools.ServiceLayer.WorkspaceServices.Contracts; namespace Microsoft.SqlTools.ServiceLayer.WorkspaceServices { /// /// Manages a "workspace" of script files that are open for a particular /// editing session. Also helps to navigate references between ScriptFiles. /// public class Workspace : IDisposable { #region Private Fields private Dictionary workspaceFiles = new Dictionary(); #endregion #region Properties /// /// Gets or sets the root path of the workspace. /// public string WorkspacePath { get; set; } #endregion #region Constructors /// /// Creates a new instance of the Workspace class. /// public Workspace() { } #endregion #region Public Methods /// /// Gets an open file in the workspace. If the file isn't open but /// exists on the filesystem, load and return it. /// /// The file path at which the script resides. /// /// is not found. /// /// /// contains a null or empty string. /// public ScriptFile GetFile(string filePath) { Validate.IsNotNullOrEmptyString("filePath", filePath); // Resolve the full file path string resolvedFilePath = this.ResolveFilePath(filePath); string keyName = resolvedFilePath.ToLower(); // Make sure the file isn't already loaded into the workspace ScriptFile scriptFile = null; if (!this.workspaceFiles.TryGetValue(keyName, out scriptFile)) { // This method allows FileNotFoundException to bubble up // if the file isn't found. using (FileStream fileStream = new FileStream(resolvedFilePath, FileMode.Open, FileAccess.Read)) using (StreamReader streamReader = new StreamReader(fileStream, Encoding.UTF8)) { scriptFile = new ScriptFile(resolvedFilePath, filePath,streamReader); this.workspaceFiles.Add(keyName, scriptFile); } Logger.Write(LogLevel.Verbose, "Opened file on disk: " + resolvedFilePath); } return scriptFile; } private string ResolveFilePath(string filePath) { if (!IsPathInMemory(filePath)) { if (filePath.StartsWith(@"file://")) { // Client sent the path in URI format, extract the local path and trim // any extraneous slashes Uri fileUri = new Uri(filePath); filePath = fileUri.LocalPath.TrimStart('/'); } // Some clients send paths with UNIX-style slashes, replace those if necessary filePath = filePath.Replace('/', '\\'); // Clients could specify paths with escaped space, [ and ] characters which .NET APIs // will not handle. These paths will get appropriately escaped just before being passed // into the SqlTools engine. filePath = UnescapePath(filePath); // Get the absolute file path filePath = Path.GetFullPath(filePath); } Logger.Write(LogLevel.Verbose, "Resolved path: " + filePath); return filePath; } internal static bool IsPathInMemory(string filePath) { // When viewing SqlTools files in the Git diff viewer, VS Code // sends the contents of the file at HEAD with a URI that starts // with 'inmemory'. Untitled files which have been marked of // type SqlTools have a path starting with 'untitled'. return filePath.StartsWith("inmemory") || filePath.StartsWith("untitled"); } /// /// Unescapes any escaped [, ] or space characters. Typically use this before calling a /// .NET API that doesn't understand PowerShell escaped chars. /// /// The path to unescape. /// The path with the ` character before [, ] and spaces removed. public static string UnescapePath(string path) { if (!path.Contains("`")) { return path; } return Regex.Replace(path, @"`(?=[ \[\]])", ""); } /// /// Gets a new ScriptFile instance which is identified by the given file /// path and initially contains the given buffer contents. /// /// /// /// public ScriptFile GetFileBuffer(string filePath, string initialBuffer) { Validate.IsNotNullOrEmptyString("filePath", filePath); // Resolve the full file path string resolvedFilePath = this.ResolveFilePath(filePath); string keyName = resolvedFilePath.ToLower(); // Make sure the file isn't already loaded into the workspace ScriptFile scriptFile = null; if (!this.workspaceFiles.TryGetValue(keyName, out scriptFile)) { scriptFile = new ScriptFile(resolvedFilePath, filePath, initialBuffer); this.workspaceFiles.Add(keyName, scriptFile); Logger.Write(LogLevel.Verbose, "Opened file as in-memory buffer: " + resolvedFilePath); } return scriptFile; } /// /// Gets an array of all opened ScriptFiles in the workspace. /// /// An array of all opened ScriptFiles in the workspace. public ScriptFile[] GetOpenedFiles() { return workspaceFiles.Values.ToArray(); } /// /// Closes a currently open script file with the given file path. /// /// The file path at which the script resides. public void CloseFile(ScriptFile scriptFile) { Validate.IsNotNull("scriptFile", scriptFile); this.workspaceFiles.Remove(scriptFile.Id); } private string GetBaseFilePath(string filePath) { if (IsPathInMemory(filePath)) { // If the file is in memory, use the workspace path return this.WorkspacePath; } if (!Path.IsPathRooted(filePath)) { // TODO: Assert instead? throw new InvalidOperationException( string.Format( "Must provide a full path for originalScriptPath: {0}", filePath)); } // Get the directory of the file path return Path.GetDirectoryName(filePath); } private string ResolveRelativeScriptPath(string baseFilePath, string relativePath) { if (Path.IsPathRooted(relativePath)) { return relativePath; } // Get the directory of the original script file, combine it // with the given path and then resolve the absolute file path. string combinedPath = Path.GetFullPath( Path.Combine( baseFilePath, relativePath)); return combinedPath; } #endregion #region IDisposable Implementation /// /// Disposes of any Runspaces that were created for the /// services used in this session. /// public void Dispose() { } #endregion } }