Initial commit of SqlTools Service API

This commit is contained in:
Karl Burtram
2016-07-15 11:02:03 -07:00
parent 874ade9001
commit 790825cfab
112 changed files with 9782 additions and 1 deletions

View File

@@ -0,0 +1,111 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using System.Diagnostics;
namespace Microsoft.PowerShell.EditorServices
{
/// <summary>
/// Provides details about a position in a file buffer. All
/// positions are expressed in 1-based positions (i.e. the
/// first line and column in the file is position 1,1).
/// </summary>
[DebuggerDisplay("Position = {Line}:{Column}")]
public class BufferPosition
{
#region Properties
/// <summary>
/// Provides an instance that represents a position that has not been set.
/// </summary>
public static readonly BufferPosition None = new BufferPosition(-1, -1);
/// <summary>
/// Gets the line number of the position in the buffer.
/// </summary>
public int Line { get; private set; }
/// <summary>
/// Gets the column number of the position in the buffer.
/// </summary>
public int Column { get; private set; }
#endregion
#region Constructors
/// <summary>
/// Creates a new instance of the BufferPosition class.
/// </summary>
/// <param name="line">The line number of the position.</param>
/// <param name="column">The column number of the position.</param>
public BufferPosition(int line, int column)
{
this.Line = line;
this.Column = column;
}
#endregion
#region Public Methods
/// <summary>
/// Compares two instances of the BufferPosition class.
/// </summary>
/// <param name="obj">The object to which this instance will be compared.</param>
/// <returns>True if the positions are equal, false otherwise.</returns>
public override bool Equals(object obj)
{
if (!(obj is BufferPosition))
{
return false;
}
BufferPosition other = (BufferPosition)obj;
return
this.Line == other.Line &&
this.Column == other.Column;
}
/// <summary>
/// Calculates a unique hash code that represents this instance.
/// </summary>
/// <returns>A hash code representing this instance.</returns>
public override int GetHashCode()
{
return this.Line.GetHashCode() ^ this.Column.GetHashCode();
}
/// <summary>
/// Compares two positions to check if one is greater than the other.
/// </summary>
/// <param name="positionOne">The first position to compare.</param>
/// <param name="positionTwo">The second position to compare.</param>
/// <returns>True if positionOne is greater than positionTwo.</returns>
public static bool operator >(BufferPosition positionOne, BufferPosition positionTwo)
{
return
(positionOne != null && positionTwo == null) ||
(positionOne.Line > positionTwo.Line) ||
(positionOne.Line == positionTwo.Line &&
positionOne.Column > positionTwo.Column);
}
/// <summary>
/// Compares two positions to check if one is less than the other.
/// </summary>
/// <param name="positionOne">The first position to compare.</param>
/// <param name="positionTwo">The second position to compare.</param>
/// <returns>True if positionOne is less than positionTwo.</returns>
public static bool operator <(BufferPosition positionOne, BufferPosition positionTwo)
{
return positionTwo > positionOne;
}
#endregion
}
}

View File

@@ -0,0 +1,123 @@
//
// 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.Diagnostics;
namespace Microsoft.PowerShell.EditorServices
{
/// <summary>
/// Provides details about a range between two positions in
/// a file buffer.
/// </summary>
[DebuggerDisplay("Start = {Start.Line}:{Start.Column}, End = {End.Line}:{End.Column}")]
public class BufferRange
{
#region Properties
/// <summary>
/// Provides an instance that represents a range that has not been set.
/// </summary>
public static readonly BufferRange None = new BufferRange(0, 0, 0, 0);
/// <summary>
/// Gets the start position of the range in the buffer.
/// </summary>
public BufferPosition Start { get; private set; }
/// <summary>
/// Gets the end position of the range in the buffer.
/// </summary>
public BufferPosition End { get; private set; }
/// <summary>
/// Returns true if the current range is non-zero, i.e.
/// contains valid start and end positions.
/// </summary>
public bool HasRange
{
get
{
return this.Equals(BufferRange.None);
}
}
#endregion
#region Constructors
/// <summary>
/// Creates a new instance of the BufferRange class.
/// </summary>
/// <param name="start">The start position of the range.</param>
/// <param name="end">The end position of the range.</param>
public BufferRange(BufferPosition start, BufferPosition end)
{
if (start > end)
{
throw new ArgumentException(
string.Format(
"Start position ({0}, {1}) must come before or be equal to the end position ({2}, {3}).",
start.Line, start.Column,
end.Line, end.Column));
}
this.Start = start;
this.End = end;
}
/// <summary>
/// Creates a new instance of the BufferRange class.
/// </summary>
/// <param name="startLine">The 1-based starting line number of the range.</param>
/// <param name="startColumn">The 1-based starting column number of the range.</param>
/// <param name="endLine">The 1-based ending line number of the range.</param>
/// <param name="endColumn">The 1-based ending column number of the range.</param>
public BufferRange(
int startLine,
int startColumn,
int endLine,
int endColumn)
{
this.Start = new BufferPosition(startLine, startColumn);
this.End = new BufferPosition(endLine, endColumn);
}
#endregion
#region Public Methods
/// <summary>
/// Compares two instances of the BufferRange class.
/// </summary>
/// <param name="obj">The object to which this instance will be compared.</param>
/// <returns>True if the ranges are equal, false otherwise.</returns>
public override bool Equals(object obj)
{
if (!(obj is BufferRange))
{
return false;
}
BufferRange other = (BufferRange)obj;
return
this.Start.Equals(other.Start) &&
this.End.Equals(other.End);
}
/// <summary>
/// Calculates a unique hash code that represents this instance.
/// </summary>
/// <returns>A hash code representing this instance.</returns>
public override int GetHashCode()
{
return this.Start.GetHashCode() ^ this.End.GetHashCode();
}
#endregion
}
}

View File

@@ -0,0 +1,38 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
namespace Microsoft.PowerShell.EditorServices
{
/// <summary>
/// Contains details relating to a content change in an open file.
/// </summary>
public class FileChange
{
/// <summary>
/// The string which is to be inserted in the file.
/// </summary>
public string InsertString { get; set; }
/// <summary>
/// The 1-based line number where the change starts.
/// </summary>
public int Line { get; set; }
/// <summary>
/// The 1-based column offset where the change starts.
/// </summary>
public int Offset { get; set; }
/// <summary>
/// The 1-based line number where the change ends.
/// </summary>
public int EndLine { get; set; }
/// <summary>
/// The 1-based column offset where the change ends.
/// </summary>
public int EndOffset { get; set; }
}
}

View File

@@ -0,0 +1,118 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
namespace Microsoft.PowerShell.EditorServices
{
/// <summary>
/// Provides details and operations for a buffer position in a
/// specific file.
/// </summary>
public class FilePosition : BufferPosition
{
public FilePosition(
ScriptFile scriptFile,
int line,
int column)
: base(line, column)
{
}
#if false
#region Private Fields
private ScriptFile scriptFile;
#endregion
#region Constructors
/// <summary>
/// Creates a new FilePosition instance for the 1-based line and
/// column numbers in the specified file.
/// </summary>
/// <param name="scriptFile">The ScriptFile in which the position is located.</param>
/// <param name="line">The 1-based line number in the file.</param>
/// <param name="column">The 1-based column number in the file.</param>
public FilePosition(
ScriptFile scriptFile,
int line,
int column)
: base(line, column)
{
this.scriptFile = scriptFile;
}
/// <summary>
/// Creates a new FilePosition instance for the specified file by
/// copying the specified BufferPosition
/// </summary>
/// <param name="scriptFile">The ScriptFile in which the position is located.</param>
/// <param name="copiedPosition">The original BufferPosition from which the line and column will be copied.</param>
public FilePosition(
ScriptFile scriptFile,
BufferPosition copiedPosition)
: this(scriptFile, copiedPosition.Line, copiedPosition.Column)
{
scriptFile.ValidatePosition(copiedPosition);
}
#endregion
#region Public Methods
/// <summary>
/// Gets a FilePosition relative to this position by adding the
/// provided line and column offset relative to the contents of
/// the current file.
/// </summary>
/// <param name="lineOffset">The line offset to add to this position.</param>
/// <param name="columnOffset">The column offset to add to this position.</param>
/// <returns>A new FilePosition instance for the calculated position.</returns>
public FilePosition AddOffset(int lineOffset, int columnOffset)
{
return this.scriptFile.CalculatePosition(
this,
lineOffset,
columnOffset);
}
/// <summary>
/// Gets a FilePosition for the line and column position
/// of the beginning of the current line after any initial
/// whitespace for indentation.
/// </summary>
/// <returns>A new FilePosition instance for the calculated position.</returns>
public FilePosition GetLineStart()
{
string scriptLine = scriptFile.FileLines[this.Line - 1];
int lineStartColumn = 1;
for (int i = 0; i < scriptLine.Length; i++)
{
if (!char.IsWhiteSpace(scriptLine[i]))
{
lineStartColumn = i + 1;
break;
}
}
return new FilePosition(this.scriptFile, this.Line, lineStartColumn);
}
/// <summary>
/// Gets a FilePosition for the line and column position
/// of the end of the current line.
/// </summary>
/// <returns>A new FilePosition instance for the calculated position.</returns>
public FilePosition GetLineEnd()
{
string scriptLine = scriptFile.FileLines[this.Line - 1];
return new FilePosition(this.scriptFile, this.Line, scriptLine.Length + 1);
}
#endregion
#endif
}
}

View File

@@ -0,0 +1,558 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using Microsoft.PowerShell.EditorServices.Utility;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
//using System.Management.Automation;
//using System.Management.Automation.Language;
namespace Microsoft.PowerShell.EditorServices
{
/// <summary>
/// Contains the details and contents of an open script file.
/// </summary>
public class ScriptFile
{
#if false
#region Private Fields
private Token[] scriptTokens;
private Version powerShellVersion;
#endregion
#region Properties
/// <summary>
/// Gets a unique string that identifies this file. At this time,
/// this property returns a normalized version of the value stored
/// in the FilePath property.
/// </summary>
public string Id
{
get { return this.FilePath.ToLower(); }
}
/// <summary>
/// Gets the path at which this file resides.
/// </summary>
public string FilePath { get; private set; }
/// <summary>
/// Gets the path which the editor client uses to identify this file.
/// </summary>
public string ClientFilePath { get; private set; }
/// <summary>
/// Gets or sets a boolean that determines whether
/// semantic analysis should be enabled for this file.
/// For internal use only.
/// </summary>
internal bool IsAnalysisEnabled { get; set; }
/// <summary>
/// Gets a boolean that determines whether this file is
/// in-memory or not (either unsaved or non-file content).
/// </summary>
public bool IsInMemory { get; private set; }
/// <summary>
/// Gets a string containing the full contents of the file.
/// </summary>
public string Contents
{
get
{
return string.Join("\r\n", this.FileLines);
}
}
/// <summary>
/// Gets a BufferRange that represents the entire content
/// range of the file.
/// </summary>
public BufferRange FileRange { get; private set; }
/// <summary>
/// Gets the list of syntax markers found by parsing this
/// file's contents.
/// </summary>
public ScriptFileMarker[] SyntaxMarkers
{
get;
private set;
}
/// <summary>
/// Gets the list of strings for each line of the file.
/// </summary>
internal IList<string> FileLines
{
get;
private set;
}
/// <summary>
/// Gets the ScriptBlockAst representing the parsed script contents.
/// </summary>
public ScriptBlockAst ScriptAst
{
get;
private set;
}
/// <summary>
/// Gets the array of Tokens representing the parsed script contents.
/// </summary>
public Token[] ScriptTokens
{
get { return this.scriptTokens; }
}
/// <summary>
/// Gets the array of filepaths dot sourced in this ScriptFile
/// </summary>
public string[] ReferencedFiles
{
get;
private set;
}
#endregion
#region Constructors
/// <summary>
/// Creates a new ScriptFile instance by reading file contents from
/// the given TextReader.
/// </summary>
/// <param name="filePath">The path at which the script file resides.</param>
/// <param name="clientFilePath">The path which the client uses to identify the file.</param>
/// <param name="textReader">The TextReader to use for reading the file's contents.</param>
/// <param name="powerShellVersion">The version of PowerShell for which the script is being parsed.</param>
public ScriptFile(
string filePath,
string clientFilePath,
TextReader textReader,
Version powerShellVersion)
{
this.FilePath = filePath;
this.ClientFilePath = clientFilePath;
this.IsAnalysisEnabled = true;
this.IsInMemory = Workspace.IsPathInMemory(filePath);
this.powerShellVersion = powerShellVersion;
this.SetFileContents(textReader.ReadToEnd());
}
/// <summary>
/// Creates a new ScriptFile instance with the specified file contents.
/// </summary>
/// <param name="filePath">The path at which the script file resides.</param>
/// <param name="clientFilePath">The path which the client uses to identify the file.</param>
/// <param name="initialBuffer">The initial contents of the script file.</param>
/// <param name="powerShellVersion">The version of PowerShell for which the script is being parsed.</param>
public ScriptFile(
string filePath,
string clientFilePath,
string initialBuffer,
Version powerShellVersion)
{
this.FilePath = filePath;
this.ClientFilePath = clientFilePath;
this.IsAnalysisEnabled = true;
this.powerShellVersion = powerShellVersion;
this.SetFileContents(initialBuffer);
}
#endregion
#region Public Methods
/// <summary>
/// Gets a line from the file's contents.
/// </summary>
/// <param name="lineNumber">The 1-based line number in the file.</param>
/// <returns>The complete line at the given line number.</returns>
public string GetLine(int lineNumber)
{
Validate.IsWithinRange(
"lineNumber", lineNumber,
1, this.FileLines.Count + 1);
return this.FileLines[lineNumber - 1];
}
/// <summary>
/// Gets a range of lines from the file's contents.
/// </summary>
/// <param name="bufferRange">The buffer range from which lines will be extracted.</param>
/// <returns>An array of strings from the specified range of the file.</returns>
public string[] GetLinesInRange(BufferRange bufferRange)
{
this.ValidatePosition(bufferRange.Start);
this.ValidatePosition(bufferRange.End);
List<string> linesInRange = new List<string>();
int startLine = bufferRange.Start.Line,
endLine = bufferRange.End.Line;
for (int line = startLine; line <= endLine; line++)
{
string currentLine = this.FileLines[line - 1];
int startColumn =
line == startLine
? bufferRange.Start.Column
: 1;
int endColumn =
line == endLine
? bufferRange.End.Column
: currentLine.Length + 1;
currentLine =
currentLine.Substring(
startColumn - 1,
endColumn - startColumn);
linesInRange.Add(currentLine);
}
return linesInRange.ToArray();
}
/// <summary>
/// Throws ArgumentOutOfRangeException if the given position is outside
/// of the file's buffer extents.
/// </summary>
/// <param name="bufferPosition">The position in the buffer to be validated.</param>
public void ValidatePosition(BufferPosition bufferPosition)
{
this.ValidatePosition(
bufferPosition.Line,
bufferPosition.Column);
}
/// <summary>
/// Throws ArgumentOutOfRangeException if the given position is outside
/// of the file's buffer extents.
/// </summary>
/// <param name="line">The 1-based line to be validated.</param>
/// <param name="column">The 1-based column to be validated.</param>
public void ValidatePosition(int line, int column)
{
if (line < 1 || line > this.FileLines.Count + 1)
{
throw new ArgumentOutOfRangeException("Position is outside of file line range.");
}
// The maximum column is either one past the length of the string
// or 1 if the string is empty.
string lineString = this.FileLines[line - 1];
int maxColumn = lineString.Length > 0 ? lineString.Length + 1 : 1;
if (column < 1 || column > maxColumn)
{
throw new ArgumentOutOfRangeException(
string.Format(
"Position is outside of column range for line {0}.",
line));
}
}
/// <summary>
/// Applies the provided FileChange to the file's contents
/// </summary>
/// <param name="fileChange">The FileChange to apply to the file's contents.</param>
public void ApplyChange(FileChange fileChange)
{
this.ValidatePosition(fileChange.Line, fileChange.Offset);
this.ValidatePosition(fileChange.EndLine, fileChange.EndOffset);
// Break up the change lines
string[] changeLines = fileChange.InsertString.Split('\n');
// Get the first fragment of the first line
string firstLineFragment =
this.FileLines[fileChange.Line - 1]
.Substring(0, fileChange.Offset - 1);
// Get the last fragment of the last line
string endLine = this.FileLines[fileChange.EndLine - 1];
string lastLineFragment =
endLine.Substring(
fileChange.EndOffset - 1,
(this.FileLines[fileChange.EndLine - 1].Length - fileChange.EndOffset) + 1);
// Remove the old lines
for (int i = 0; i <= fileChange.EndLine - fileChange.Line; i++)
{
this.FileLines.RemoveAt(fileChange.Line - 1);
}
// Build and insert the new lines
int currentLineNumber = fileChange.Line;
for (int changeIndex = 0; changeIndex < changeLines.Length; changeIndex++)
{
// Since we split the lines above using \n, make sure to
// trim the ending \r's off as well.
string finalLine = changeLines[changeIndex].TrimEnd('\r');
// Should we add first or last line fragments?
if (changeIndex == 0)
{
// Append the first line fragment
finalLine = firstLineFragment + finalLine;
}
if (changeIndex == changeLines.Length - 1)
{
// Append the last line fragment
finalLine = finalLine + lastLineFragment;
}
this.FileLines.Insert(currentLineNumber - 1, finalLine);
currentLineNumber++;
}
// Parse the script again to be up-to-date
this.ParseFileContents();
}
/// <summary>
/// Calculates the zero-based character offset of a given
/// line and column position in the file.
/// </summary>
/// <param name="lineNumber">The 1-based line number from which the offset is calculated.</param>
/// <param name="columnNumber">The 1-based column number from which the offset is calculated.</param>
/// <returns>The zero-based offset for the given file position.</returns>
public int GetOffsetAtPosition(int lineNumber, int columnNumber)
{
Validate.IsWithinRange("lineNumber", lineNumber, 1, this.FileLines.Count);
Validate.IsGreaterThan("columnNumber", columnNumber, 0);
int offset = 0;
for(int i = 0; i < lineNumber; i++)
{
if (i == lineNumber - 1)
{
// Subtract 1 to account for 1-based column numbering
offset += columnNumber - 1;
}
else
{
// Add an offset to account for the current platform's newline characters
offset += this.FileLines[i].Length + Environment.NewLine.Length;
}
}
return offset;
}
/// <summary>
/// Calculates a FilePosition relative to a starting BufferPosition
/// using the given 1-based line and column offset.
/// </summary>
/// <param name="originalPosition">The original BufferPosition from which an new position should be calculated.</param>
/// <param name="lineOffset">The 1-based line offset added to the original position in this file.</param>
/// <param name="columnOffset">The 1-based column offset added to the original position in this file.</param>
/// <returns>A new FilePosition instance with the resulting line and column number.</returns>
public FilePosition CalculatePosition(
BufferPosition originalPosition,
int lineOffset,
int columnOffset)
{
int newLine = originalPosition.Line + lineOffset,
newColumn = originalPosition.Column + columnOffset;
this.ValidatePosition(newLine, newColumn);
string scriptLine = this.FileLines[newLine - 1];
newColumn = Math.Min(scriptLine.Length + 1, newColumn);
return new FilePosition(this, newLine, newColumn);
}
/// <summary>
/// Calculates the 1-based line and column number position based
/// on the given buffer offset.
/// </summary>
/// <param name="bufferOffset">The buffer offset to convert.</param>
/// <returns>A new BufferPosition containing the position of the offset.</returns>
public BufferPosition GetPositionAtOffset(int bufferOffset)
{
BufferRange bufferRange =
GetRangeBetweenOffsets(
bufferOffset, bufferOffset);
return bufferRange.Start;
}
/// <summary>
/// Calculates the 1-based line and column number range based on
/// the given start and end buffer offsets.
/// </summary>
/// <param name="startOffset">The start offset of the range.</param>
/// <param name="endOffset">The end offset of the range.</param>
/// <returns>A new BufferRange containing the positions in the offset range.</returns>
public BufferRange GetRangeBetweenOffsets(int startOffset, int endOffset)
{
bool foundStart = false;
int currentOffset = 0;
int searchedOffset = startOffset;
BufferPosition startPosition = new BufferPosition(0, 0);
BufferPosition endPosition = startPosition;
int line = 0;
while (line < this.FileLines.Count)
{
if (searchedOffset <= currentOffset + this.FileLines[line].Length)
{
int column = searchedOffset - currentOffset;
// Have we already found the start position?
if (foundStart)
{
// Assign the end position and end the search
endPosition = new BufferPosition(line + 1, column + 1);
break;
}
else
{
startPosition = new BufferPosition(line + 1, column + 1);
// Do we only need to find the start position?
if (startOffset == endOffset)
{
endPosition = startPosition;
break;
}
else
{
// Since the end offset can be on the same line,
// skip the line increment and continue searching
// for the end position
foundStart = true;
searchedOffset = endOffset;
continue;
}
}
}
// Increase the current offset and include newline length
currentOffset += this.FileLines[line].Length + Environment.NewLine.Length;
line++;
}
return new BufferRange(startPosition, endPosition);
}
#endregion
#region Private Methods
private void SetFileContents(string fileContents)
{
// Split the file contents into lines and trim
// any carriage returns from the strings.
this.FileLines =
fileContents
.Split('\n')
.Select(line => line.TrimEnd('\r'))
.ToList();
// Parse the contents to get syntax tree and errors
this.ParseFileContents();
}
/// <summary>
/// Parses the current file contents to get the AST, tokens,
/// and parse errors.
/// </summary>
private void ParseFileContents()
{
ParseError[] parseErrors = null;
// First, get the updated file range
int lineCount = this.FileLines.Count;
if (lineCount > 0)
{
this.FileRange =
new BufferRange(
new BufferPosition(1, 1),
new BufferPosition(
lineCount + 1,
this.FileLines[lineCount - 1].Length + 1));
}
else
{
this.FileRange = BufferRange.None;
}
try
{
#if PowerShellv5r2
// This overload appeared with Windows 10 Update 1
if (this.powerShellVersion.Major >= 5 &&
this.powerShellVersion.Build >= 10586)
{
// Include the file path so that module relative
// paths are evaluated correctly
this.ScriptAst =
Parser.ParseInput(
this.Contents,
this.FilePath,
out this.scriptTokens,
out parseErrors);
}
else
{
this.ScriptAst =
Parser.ParseInput(
this.Contents,
out this.scriptTokens,
out parseErrors);
}
#else
this.ScriptAst =
Parser.ParseInput(
this.Contents,
out this.scriptTokens,
out parseErrors);
#endif
}
catch (RuntimeException ex)
{
var parseError =
new ParseError(
null,
ex.ErrorRecord.FullyQualifiedErrorId,
ex.Message);
parseErrors = new[] { parseError };
this.scriptTokens = new Token[0];
this.ScriptAst = null;
}
// Translate parse errors into syntax markers
this.SyntaxMarkers =
parseErrors
.Select(ScriptFileMarker.FromParseError)
.ToArray();
//Get all dot sourced referenced files and store them
this.ReferencedFiles =
AstOperations.FindDotSourcedIncludes(this.ScriptAst);
}
#endregion
#endif
}
}

View File

@@ -0,0 +1,118 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using Microsoft.PowerShell.EditorServices.Utility;
using System;
//using System.Management.Automation.Language;
#if ScriptAnalyzer
using Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic;
#endif
namespace Microsoft.PowerShell.EditorServices
{
/// <summary>
/// Defines the message level of a script file marker.
/// </summary>
public enum ScriptFileMarkerLevel
{
/// <summary>
/// The marker represents an informational message.
/// </summary>
Information = 0,
/// <summary>
/// The marker represents a warning message.
/// </summary>
Warning,
/// <summary>
/// The marker represents an error message.
/// </summary>
Error
};
/// <summary>
/// Contains details about a marker that should be displayed
/// for the a script file. The marker information could come
/// from syntax parsing or semantic analysis of the script.
/// </summary>
public class ScriptFileMarker
{
#region Properties
/// <summary>
/// Gets or sets the marker's message string.
/// </summary>
public string Message { get; set; }
/// <summary>
/// Gets or sets the marker's message level.
/// </summary>
public ScriptFileMarkerLevel Level { get; set; }
/// <summary>
/// Gets or sets the ScriptRegion where the marker should appear.
/// </summary>
public ScriptRegion ScriptRegion { get; set; }
#endregion
#region Public Methods
#if false
internal static ScriptFileMarker FromParseError(
ParseError parseError)
{
Validate.IsNotNull("parseError", parseError);
return new ScriptFileMarker
{
Message = parseError.Message,
Level = ScriptFileMarkerLevel.Error,
ScriptRegion = ScriptRegion.Create(parseError.Extent)
};
}
#endif
#if ScriptAnalyzer
internal static ScriptFileMarker FromDiagnosticRecord(
DiagnosticRecord diagnosticRecord)
{
Validate.IsNotNull("diagnosticRecord", diagnosticRecord);
return new ScriptFileMarker
{
Message = diagnosticRecord.Message,
Level = GetMarkerLevelFromDiagnosticSeverity(diagnosticRecord.Severity),
ScriptRegion = ScriptRegion.Create(diagnosticRecord.Extent)
};
}
private static ScriptFileMarkerLevel GetMarkerLevelFromDiagnosticSeverity(
DiagnosticSeverity diagnosticSeverity)
{
switch (diagnosticSeverity)
{
case DiagnosticSeverity.Information:
return ScriptFileMarkerLevel.Information;
case DiagnosticSeverity.Warning:
return ScriptFileMarkerLevel.Warning;
case DiagnosticSeverity.Error:
return ScriptFileMarkerLevel.Error;
default:
throw new ArgumentException(
string.Format(
"The provided DiagnosticSeverity value '{0}' is unknown.",
diagnosticSeverity),
"diagnosticSeverity");
}
}
#endif
#endregion
}
}

View File

@@ -0,0 +1,89 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
//using System.Management.Automation.Language;
namespace Microsoft.PowerShell.EditorServices
{
/// <summary>
/// Contains details about a specific region of text in script file.
/// </summary>
public sealed class ScriptRegion
{
#region Properties
/// <summary>
/// Gets the file path of the script file in which this region is contained.
/// </summary>
public string File { get; set; }
/// <summary>
/// Gets or sets the text that is contained within the region.
/// </summary>
public string Text { get; set; }
/// <summary>
/// Gets or sets the starting line number of the region.
/// </summary>
public int StartLineNumber { get; set; }
/// <summary>
/// Gets or sets the starting column number of the region.
/// </summary>
public int StartColumnNumber { get; set; }
/// <summary>
/// Gets or sets the starting file offset of the region.
/// </summary>
public int StartOffset { get; set; }
/// <summary>
/// Gets or sets the ending line number of the region.
/// </summary>
public int EndLineNumber { get; set; }
/// <summary>
/// Gets or sets the ending column number of the region.
/// </summary>
public int EndColumnNumber { get; set; }
/// <summary>
/// Gets or sets the ending file offset of the region.
/// </summary>
public int EndOffset { get; set; }
#endregion
#region Constructors
#if false
/// <summary>
/// Creates a new instance of the ScriptRegion class from an
/// instance of an IScriptExtent implementation.
/// </summary>
/// <param name="scriptExtent">
/// The IScriptExtent to copy into the ScriptRegion.
/// </param>
/// <returns>
/// A new ScriptRegion instance with the same details as the IScriptExtent.
/// </returns>
public static ScriptRegion Create(IScriptExtent scriptExtent)
{
return new ScriptRegion
{
File = scriptExtent.File,
Text = scriptExtent.Text,
StartLineNumber = scriptExtent.StartLineNumber,
StartColumnNumber = scriptExtent.StartColumnNumber,
StartOffset = scriptExtent.StartOffset,
EndLineNumber = scriptExtent.EndLineNumber,
EndColumnNumber = scriptExtent.EndColumnNumber,
EndOffset = scriptExtent.EndOffset
};
}
#endif
#endregion
}
}

View File

@@ -0,0 +1,307 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using Microsoft.PowerShell.EditorServices.Utility;
using System;
using System.Collections.Generic;
using System.Linq;
using System.IO;
using System.Text;
namespace Microsoft.PowerShell.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
{
#if false
#region Private Fields
private Version powerShellVersion;
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="powerShellVersion">The version of PowerShell for which scripts will be parsed.</param>
public Workspace(Version powerShellVersion)
{
this.powerShellVersion = powerShellVersion;
}
#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.powerShellVersion);
this.workspaceFiles.Add(keyName, scriptFile);
}
Logger.Write(LogLevel.Verbose, "Opened file on disk: " + resolvedFilePath);
}
return scriptFile;
}
/// <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.powerShellVersion);
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 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 PowerShell engine.
filePath = PowerShellContext.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 PowerShell 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 PowerShell have a path starting with 'untitled'.
return
filePath.StartsWith("inmemory") ||
filePath.StartsWith("untitled");
}
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
}
}