// // 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.Linq; //using System.Management.Automation; //using System.Management.Automation.Language; namespace Microsoft.SqlTools.EditorServices { /// /// Contains the details and contents of an open script file. /// public class ScriptFile { #if false #region Private Fields private Token[] scriptTokens; private Version SqlToolsVersion; #endregion #region Properties /// /// 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. /// public string Id { get { return this.FilePath.ToLower(); } } /// /// Gets the path at which this file resides. /// public string FilePath { get; private set; } /// /// Gets the path which the editor client uses to identify this file. /// public string ClientFilePath { get; private set; } /// /// Gets or sets a boolean that determines whether /// semantic analysis should be enabled for this file. /// For internal use only. /// internal bool IsAnalysisEnabled { get; set; } /// /// Gets a boolean that determines whether this file is /// in-memory or not (either unsaved or non-file content). /// public bool IsInMemory { get; private set; } /// /// Gets a string containing the full contents of the file. /// public string Contents { get { return string.Join("\r\n", this.FileLines); } } /// /// Gets a BufferRange that represents the entire content /// range of the file. /// public BufferRange FileRange { get; private set; } /// /// Gets the list of syntax markers found by parsing this /// file's contents. /// public ScriptFileMarker[] SyntaxMarkers { get; private set; } /// /// Gets the list of strings for each line of the file. /// internal IList FileLines { get; private set; } /// /// Gets the ScriptBlockAst representing the parsed script contents. /// public ScriptBlockAst ScriptAst { get; private set; } /// /// Gets the array of Tokens representing the parsed script contents. /// public Token[] ScriptTokens { get { return this.scriptTokens; } } /// /// Gets the array of filepaths dot sourced in this ScriptFile /// public string[] ReferencedFiles { get; private set; } #endregion #region Constructors /// /// Creates a new ScriptFile instance by reading file contents from /// the given TextReader. /// /// The path at which the script file resides. /// The path which the client uses to identify the file. /// The TextReader to use for reading the file's contents. /// The version of SqlTools for which the script is being parsed. public ScriptFile( string filePath, string clientFilePath, TextReader textReader, Version SqlToolsVersion) { this.FilePath = filePath; this.ClientFilePath = clientFilePath; this.IsAnalysisEnabled = true; this.IsInMemory = Workspace.IsPathInMemory(filePath); this.SqlToolsVersion = SqlToolsVersion; this.SetFileContents(textReader.ReadToEnd()); } /// /// Creates a new ScriptFile instance with the specified file contents. /// /// The path at which the script file resides. /// The path which the client uses to identify the file. /// The initial contents of the script file. /// The version of SqlTools for which the script is being parsed. public ScriptFile( string filePath, string clientFilePath, string initialBuffer, Version SqlToolsVersion) { this.FilePath = filePath; this.ClientFilePath = clientFilePath; this.IsAnalysisEnabled = true; this.SqlToolsVersion = SqlToolsVersion; this.SetFileContents(initialBuffer); } #endregion #region Public Methods /// /// Gets a line from the file's contents. /// /// The 1-based line number in the file. /// The complete line at the given line number. public string GetLine(int lineNumber) { Validate.IsWithinRange( "lineNumber", lineNumber, 1, this.FileLines.Count + 1); return this.FileLines[lineNumber - 1]; } /// /// Gets a range of lines from the file's contents. /// /// The buffer range from which lines will be extracted. /// An array of strings from the specified range of the file. public string[] GetLinesInRange(BufferRange bufferRange) { this.ValidatePosition(bufferRange.Start); this.ValidatePosition(bufferRange.End); List linesInRange = new List(); 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(); } /// /// Throws ArgumentOutOfRangeException if the given position is outside /// of the file's buffer extents. /// /// The position in the buffer to be validated. public void ValidatePosition(BufferPosition bufferPosition) { this.ValidatePosition( bufferPosition.Line, bufferPosition.Column); } /// /// Throws ArgumentOutOfRangeException if the given position is outside /// of the file's buffer extents. /// /// The 1-based line to be validated. /// The 1-based column to be validated. 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)); } } /// /// Applies the provided FileChange to the file's contents /// /// The FileChange to apply to the file's contents. 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(); } /// /// Calculates the zero-based character offset of a given /// line and column position in the file. /// /// The 1-based line number from which the offset is calculated. /// The 1-based column number from which the offset is calculated. /// The zero-based offset for the given file position. 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; } /// /// Calculates a FilePosition relative to a starting BufferPosition /// using the given 1-based line and column offset. /// /// The original BufferPosition from which an new position should be calculated. /// The 1-based line offset added to the original position in this file. /// The 1-based column offset added to the original position in this file. /// A new FilePosition instance with the resulting line and column number. 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); } /// /// Calculates the 1-based line and column number position based /// on the given buffer offset. /// /// The buffer offset to convert. /// A new BufferPosition containing the position of the offset. public BufferPosition GetPositionAtOffset(int bufferOffset) { BufferRange bufferRange = GetRangeBetweenOffsets( bufferOffset, bufferOffset); return bufferRange.Start; } /// /// Calculates the 1-based line and column number range based on /// the given start and end buffer offsets. /// /// The start offset of the range. /// The end offset of the range. /// A new BufferRange containing the positions in the offset range. 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(); } /// /// Parses the current file contents to get the AST, tokens, /// and parse errors. /// 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 SqlToolsv5r2 // This overload appeared with Windows 10 Update 1 if (this.SqlToolsVersion.Major >= 5 && this.SqlToolsVersion.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 } }