mirror of
https://github.com/ckaczor/sqltoolsservice.git
synced 2026-01-14 01:25:40 -05:00
Also include misc. clean-ups related to removing unneeded PowerShell Language Service code.
331 lines
12 KiB
C#
331 lines
12 KiB
C#
//
|
|
// 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
|
|
{
|
|
/// <summary>
|
|
/// Manages a "workspace" of script files that are open for a particular
|
|
/// editing session. Also helps to navigate references between ScriptFiles.
|
|
/// </summary>
|
|
public class Workspace
|
|
{
|
|
#region Private Fields
|
|
|
|
private Version SqlToolsVersion;
|
|
private Dictionary<string, ScriptFile> workspaceFiles = new Dictionary<string, ScriptFile>();
|
|
|
|
#endregion
|
|
|
|
#region Properties
|
|
|
|
/// <summary>
|
|
/// Gets or sets the root path of the workspace.
|
|
/// </summary>
|
|
public string WorkspacePath { get; set; }
|
|
|
|
#endregion
|
|
|
|
#region Constructors
|
|
|
|
/// <summary>
|
|
/// Creates a new instance of the Workspace class.
|
|
/// </summary>
|
|
/// <param name="SqlToolsVersion">The version of SqlTools for which scripts will be parsed.</param>
|
|
public Workspace(Version SqlToolsVersion)
|
|
{
|
|
this.SqlToolsVersion = SqlToolsVersion;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Public Methods
|
|
|
|
/// <summary>
|
|
/// Gets an open file in the workspace. If the file isn't open but
|
|
/// exists on the filesystem, load and return it.
|
|
/// </summary>
|
|
/// <param name="filePath">The file path at which the script resides.</param>
|
|
/// <exception cref="FileNotFoundException">
|
|
/// <paramref name="filePath"/> is not found.
|
|
/// </exception>
|
|
/// <exception cref="ArgumentException">
|
|
/// <paramref name="filePath"/> contains a null or empty string.
|
|
/// </exception>
|
|
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");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Unescapes any escaped [, ] or space characters. Typically use this before calling a
|
|
/// .NET API that doesn't understand PowerShell escaped chars.
|
|
/// </summary>
|
|
/// <param name="path">The path to unescape.</param>
|
|
/// <returns>The path with the ` character before [, ] and spaces removed.</returns>
|
|
public static string UnescapePath(string path)
|
|
{
|
|
if (!path.Contains("`"))
|
|
{
|
|
return path;
|
|
}
|
|
|
|
return Regex.Replace(path, @"`(?=[ \[\]])", "");
|
|
}
|
|
|
|
#endregion
|
|
|
|
#if false
|
|
|
|
|
|
#region Public Methods
|
|
|
|
|
|
/// <summary>
|
|
/// Gets a new ScriptFile instance which is identified by the given file
|
|
/// path and initially contains the given buffer contents.
|
|
/// </summary>
|
|
/// <param name="filePath"></param>
|
|
/// <param name="initialBuffer"></param>
|
|
/// <returns></returns>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets an array of all opened ScriptFiles in the workspace.
|
|
/// </summary>
|
|
/// <returns>An array of all opened ScriptFiles in the workspace.</returns>
|
|
public ScriptFile[] GetOpenedFiles()
|
|
{
|
|
return workspaceFiles.Values.ToArray();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Closes a currently open script file with the given file path.
|
|
/// </summary>
|
|
/// <param name="scriptFile">The file path at which the script resides.</param>
|
|
public void CloseFile(ScriptFile scriptFile)
|
|
{
|
|
Validate.IsNotNull("scriptFile", scriptFile);
|
|
|
|
this.workspaceFiles.Remove(scriptFile.Id);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets all file references by recursively searching
|
|
/// through referenced files in a scriptfile
|
|
/// </summary>
|
|
/// <param name="scriptFile">Contains the details and contents of an open script file</param>
|
|
/// <returns>A scriptfile array where the first file
|
|
/// in the array is the "root file" of the search</returns>
|
|
public ScriptFile[] ExpandScriptReferences(ScriptFile scriptFile)
|
|
{
|
|
Dictionary<string, ScriptFile> referencedScriptFiles = new Dictionary<string, ScriptFile>();
|
|
List<ScriptFile> expandedReferences = new List<ScriptFile>();
|
|
|
|
// 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
|
|
|
|
/// <summary>
|
|
/// Recusrively searches through referencedFiles in scriptFiles
|
|
/// and builds a Dictonary of the file references
|
|
/// </summary>
|
|
/// <param name="scriptFile">Details an contents of "root" script file</param>
|
|
/// <param name="referencedScriptFiles">A Dictionary of referenced script files</param>
|
|
private void RecursivelyFindReferences(
|
|
ScriptFile scriptFile,
|
|
Dictionary<string, ScriptFile> 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
|
|
}
|
|
}
|