//
// 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
}
}