// // 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.Collections.Generic; using System.IO; using System.Linq; using Microsoft.SqlTools.ServiceLayer.Utility; namespace Microsoft.SqlTools.ServiceLayer.Workspace.Contracts { /// /// Contains the details and contents of an open script file. /// public class ScriptFile { #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 or sets the path which the editor client uses to identify this file. /// Setter for testing purposes only /// virtual to allow mocking. /// public virtual string ClientFilePath { get; internal 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 or sets a string containing the full contents of the file. /// Setter for testing purposes only /// public virtual string Contents { get { return string.Join("\r\n", FileLines); } set { FileLines = value != null ? value.Split('\n') : null; } } /// /// 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; } #endregion #region Constructors /// /// Add a default constructor for testing /// public ScriptFile() { ClientFilePath = "test.sql"; } /// /// 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. public ScriptFile( string filePath, string clientFilePath, TextReader textReader) { FilePath = filePath; ClientFilePath = clientFilePath; IsAnalysisEnabled = true; IsInMemory = Workspace.IsPathInMemory(filePath); 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. public ScriptFile( string filePath, string clientFilePath, string initialBuffer) { FilePath = filePath; ClientFilePath = clientFilePath; IsAnalysisEnabled = true; 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, FileLines.Count + 1); return FileLines[lineNumber - 1]; } /// /// Gets the text under a specific range /// public string GetTextInRange(BufferRange range) { return string.Join(Environment.NewLine, GetLinesInRange(range)); } /// /// Gets a range of lines from the file's contents. Virtual method to allow for /// mocking. /// /// The buffer range from which lines will be extracted. /// An array of strings from the specified range of the file. public virtual string[] GetLinesInRange(BufferRange bufferRange) { ValidatePosition(bufferRange.Start); 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 = 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) { 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 > FileLines.Count + 1) { throw new ArgumentOutOfRangeException(nameof(line), SR.WorkspaceServicePositionLineOutOfRange); } // The maximum column is either one past the length of the string // or 1 if the string is empty. string lineString = FileLines[line - 1]; int maxColumn = lineString.Length > 0 ? lineString.Length + 1 : 1; if (column < 1 || column > maxColumn) { throw new ArgumentOutOfRangeException(nameof(column), SR.WorkspaceServicePositionColumnOutOfRange(line)); } } /// /// Applies the provided FileChange to the file's contents /// /// The FileChange to apply to the file's contents. public void ApplyChange(FileChange fileChange) { ValidatePosition(fileChange.Line, fileChange.Offset); 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 = FileLines[fileChange.Line - 1] .Substring(0, fileChange.Offset - 1); // Get the last fragment of the last line string endLine = FileLines[fileChange.EndLine - 1]; string lastLineFragment = endLine.Substring( fileChange.EndOffset - 1, (FileLines[fileChange.EndLine - 1].Length - fileChange.EndOffset) + 1); // Remove the old lines for (int i = 0; i <= fileChange.EndLine - fileChange.Line; i++) { 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; } FileLines.Insert(currentLineNumber - 1, finalLine); currentLineNumber++; } } /// /// 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, 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 += 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; ValidatePosition(newLine, newColumn); string scriptLine = 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 < FileLines.Count) { if (searchedOffset <= currentOffset + 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 += FileLines[line].Length + Environment.NewLine.Length; line++; } return new BufferRange(startPosition, endPosition); } /// /// Set the script files contents /// /// public void SetFileContents(string fileContents) { // Split the file contents into lines and trim // any carriage returns from the strings. FileLines = fileContents .Split('\n') .Select(line => line.TrimEnd('\r')) .ToList(); } #endregion } }