// // Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. // using Microsoft.SqlTools.EditorServices.Utility; using System; using System.Collections.Generic; using System.IO; using System.Text; using System.Text.RegularExpressions; namespace Microsoft.SqlTools.EditorServices { /// /// Manages a "workspace" of script files that are open for a particular /// editing session. Also helps to navigate references between ScriptFiles. /// public class Workspace { #region Private Fields private Version SqlToolsVersion; 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. /// /// The version of SqlTools for which scripts will be parsed. public Workspace(Version SqlToolsVersion) { this.SqlToolsVersion = SqlToolsVersion; } #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.SqlToolsVersion); 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, @"`(?=[ \[\]])", ""); } #endregion #if false #region Public Methods /// /// 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.SqlToolsVersion); 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); } /// /// Gets all file references by recursively searching /// through referenced files in a scriptfile /// /// Contains the details and contents of an open script file /// A scriptfile array where the first file /// in the array is the "root file" of the search public ScriptFile[] ExpandScriptReferences(ScriptFile scriptFile) { Dictionary referencedScriptFiles = new Dictionary(); List expandedReferences = new List(); // add original file so it's not searched for, then find all file references referencedScriptFiles.Add(scriptFile.Id, scriptFile); RecursivelyFindReferences(scriptFile, referencedScriptFiles); // remove original file from referened file and add it as the first element of the // expanded referenced list to maintain order so the original file is always first in the list referencedScriptFiles.Remove(scriptFile.Id); expandedReferences.Add(scriptFile); if (referencedScriptFiles.Count > 0) { expandedReferences.AddRange(referencedScriptFiles.Values); } return expandedReferences.ToArray(); } #endregion #region Private Methods /// /// Recusrively searches through referencedFiles in scriptFiles /// and builds a Dictonary of the file references /// /// Details an contents of "root" script file /// A Dictionary of referenced script files private void RecursivelyFindReferences( ScriptFile scriptFile, Dictionary referencedScriptFiles) { // Get the base path of the current script for use in resolving relative paths string baseFilePath = GetBaseFilePath( scriptFile.FilePath); ScriptFile referencedFile; foreach (string referencedFileName in scriptFile.ReferencedFiles) { string resolvedScriptPath = this.ResolveRelativeScriptPath( baseFilePath, referencedFileName); // Make sure file exists before trying to get the file if (File.Exists(resolvedScriptPath)) { // Get the referenced file if it's not already in referencedScriptFiles referencedFile = this.GetFile(resolvedScriptPath); // Normalize the resolved script path and add it to the // referenced files list if it isn't there already resolvedScriptPath = resolvedScriptPath.ToLower(); if (!referencedScriptFiles.ContainsKey(resolvedScriptPath)) { referencedScriptFiles.Add(resolvedScriptPath, referencedFile); RecursivelyFindReferences(referencedFile, referencedScriptFiles); } } } } 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 #endif } }