mirror of
https://github.com/ckaczor/sqltoolsservice.git
synced 2026-01-30 09:35:38 -05:00
Fixing issues as per suggestions from @kevcunnane
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
//
|
||||
// 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.SqlTools.ServiceLayer.Workspace.Contracts
|
||||
{
|
||||
/// <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
|
||||
}
|
||||
}
|
||||
@@ -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.SqlTools.ServiceLayer.Workspace.Contracts
|
||||
{
|
||||
/// <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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
//
|
||||
// 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.ServiceLayer.Hosting.Protocol.Contracts;
|
||||
|
||||
namespace Microsoft.SqlTools.ServiceLayer.Workspace.Contracts
|
||||
{
|
||||
public class DidChangeConfigurationNotification<TConfig>
|
||||
{
|
||||
public static readonly
|
||||
EventType<DidChangeConfigurationParams<TConfig>> Type =
|
||||
EventType<DidChangeConfigurationParams<TConfig>>.Create("workspace/didChangeConfiguration");
|
||||
}
|
||||
|
||||
public class DidChangeConfigurationParams<TConfig>
|
||||
{
|
||||
public TConfig Settings { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -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.SqlTools.ServiceLayer.Workspace.Contracts
|
||||
{
|
||||
/// <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; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
namespace Microsoft.SqlTools.ServiceLayer.Workspace.Contracts
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides details and operations for a buffer position in a
|
||||
/// specific file.
|
||||
/// </summary>
|
||||
public class FilePosition : BufferPosition
|
||||
{
|
||||
#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
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,537 @@
|
||||
//
|
||||
// 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;
|
||||
|
||||
namespace Microsoft.SqlTools.ServiceLayer.Workspace.Contracts
|
||||
{
|
||||
/// <summary>
|
||||
/// Contains the details and contents of an open script file.
|
||||
/// </summary>
|
||||
public class ScriptFile
|
||||
{
|
||||
#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 array of filepaths dot sourced in this ScriptFile
|
||||
/// </summary>
|
||||
public string[] ReferencedFiles
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Constructors
|
||||
|
||||
/// <summary>
|
||||
/// Add a default constructor for testing
|
||||
/// </summary>
|
||||
public ScriptFile()
|
||||
{
|
||||
}
|
||||
|
||||
/// <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>
|
||||
public ScriptFile(
|
||||
string filePath,
|
||||
string clientFilePath,
|
||||
TextReader textReader)
|
||||
{
|
||||
this.FilePath = filePath;
|
||||
this.ClientFilePath = clientFilePath;
|
||||
this.IsAnalysisEnabled = true;
|
||||
this.IsInMemory = Workspace.IsPathInMemory(filePath);
|
||||
|
||||
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>
|
||||
public ScriptFile(
|
||||
string filePath,
|
||||
string clientFilePath,
|
||||
string initialBuffer)
|
||||
{
|
||||
this.FilePath = filePath;
|
||||
this.ClientFilePath = clientFilePath;
|
||||
this.IsAnalysisEnabled = true;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set the script files contents
|
||||
/// </summary>
|
||||
/// <param name="fileContents"></param>
|
||||
public 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();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Private Methods
|
||||
|
||||
/// <summary>
|
||||
/// Parses the current file contents to get the AST, tokens,
|
||||
/// and parse errors.
|
||||
/// </summary>
|
||||
private void ParseFileContents()
|
||||
{
|
||||
#if false
|
||||
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);
|
||||
#endif
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
namespace Microsoft.SqlTools.ServiceLayer.Workspace.Contracts
|
||||
{
|
||||
/// <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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.SqlTools.ServiceLayer.Workspace.Contracts
|
||||
{
|
||||
/// <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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
//
|
||||
// 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;
|
||||
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Contracts;
|
||||
|
||||
namespace Microsoft.SqlTools.ServiceLayer.Workspace.Contracts
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines a base parameter class for identifying a text document.
|
||||
/// </summary>
|
||||
[DebuggerDisplay("TextDocumentIdentifier = {Uri}")]
|
||||
public class TextDocumentIdentifier
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the URI which identifies the path of the
|
||||
/// text document.
|
||||
/// </summary>
|
||||
public string Uri { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Defines a position in a text document.
|
||||
/// </summary>
|
||||
[DebuggerDisplay("TextDocumentPosition = {Position.Line}:{Position.Character}")]
|
||||
public class TextDocumentPosition : TextDocumentIdentifier
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the position in the document.
|
||||
/// </summary>
|
||||
public Position Position { get; set; }
|
||||
}
|
||||
|
||||
public class DidOpenTextDocumentNotification : TextDocumentIdentifier
|
||||
{
|
||||
public static readonly
|
||||
EventType<DidOpenTextDocumentNotification> Type =
|
||||
EventType<DidOpenTextDocumentNotification>.Create("textDocument/didOpen");
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the full content of the opened document.
|
||||
/// </summary>
|
||||
public string Text { get; set; }
|
||||
}
|
||||
|
||||
public class DidCloseTextDocumentNotification
|
||||
{
|
||||
public static readonly
|
||||
EventType<TextDocumentIdentifier> Type =
|
||||
EventType<TextDocumentIdentifier>.Create("textDocument/didClose");
|
||||
}
|
||||
|
||||
public class DidChangeTextDocumentNotification
|
||||
{
|
||||
public static readonly
|
||||
EventType<DidChangeTextDocumentParams> Type =
|
||||
EventType<DidChangeTextDocumentParams>.Create("textDocument/didChange");
|
||||
}
|
||||
|
||||
public class DidChangeTextDocumentParams : TextDocumentIdentifier
|
||||
{
|
||||
public TextDocumentUriChangeEvent TextDocument { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the list of changes to the document content.
|
||||
/// </summary>
|
||||
public TextDocumentChangeEvent[] ContentChanges { get; set; }
|
||||
}
|
||||
|
||||
public class TextDocumentUriChangeEvent
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the Uri of the changed text document
|
||||
/// </summary>
|
||||
public string Uri { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Version of the changed text document
|
||||
/// </summary>
|
||||
public int Version { get; set; }
|
||||
}
|
||||
|
||||
public class TextDocumentChangeEvent
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the Range where the document was changed. Will
|
||||
/// be null if the server's TextDocumentSyncKind is Full.
|
||||
/// </summary>
|
||||
public Range? Range { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the length of the Range being replaced in the
|
||||
/// document. Will be null if the server's TextDocumentSyncKind is
|
||||
/// Full.
|
||||
/// </summary>
|
||||
public int? RangeLength { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the new text of the document.
|
||||
/// </summary>
|
||||
public string Text { get; set; }
|
||||
}
|
||||
|
||||
[DebuggerDisplay("Position = {Line}:{Character}")]
|
||||
public class Position
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the zero-based line number.
|
||||
/// </summary>
|
||||
public int Line { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the zero-based column number.
|
||||
/// </summary>
|
||||
public int Character { get; set; }
|
||||
}
|
||||
|
||||
[DebuggerDisplay("Start = {Start.Line}:{Start.Character}, End = {End.Line}:{End.Character}")]
|
||||
public struct Range
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the starting position of the range.
|
||||
/// </summary>
|
||||
public Position Start { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the ending position of the range.
|
||||
/// </summary>
|
||||
public Position End { get; set; }
|
||||
}
|
||||
|
||||
[DebuggerDisplay("Range = {Range.Start.Line}:{Range.Start.Character} - {Range.End.Line}:{Range.End.Character}, Uri = {Uri}")]
|
||||
public class Location
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the URI indicating the file in which the location refers.
|
||||
/// </summary>
|
||||
public string Uri { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Range indicating the range in which location refers.
|
||||
/// </summary>
|
||||
public Range Range { get; set; }
|
||||
}
|
||||
|
||||
public enum FileChangeType
|
||||
{
|
||||
Created = 1,
|
||||
|
||||
Changed,
|
||||
|
||||
Deleted
|
||||
}
|
||||
|
||||
public class FileEvent
|
||||
{
|
||||
public string Uri { get; set; }
|
||||
|
||||
public FileChangeType Type { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
//
|
||||
// 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.ServiceLayer.Hosting.Protocol.Contracts;
|
||||
|
||||
namespace Microsoft.SqlTools.ServiceLayer.Workspace.Contracts
|
||||
{
|
||||
public enum SymbolKind
|
||||
{
|
||||
File = 1,
|
||||
Module = 2,
|
||||
Namespace = 3,
|
||||
Package = 4,
|
||||
Class = 5,
|
||||
Method = 6,
|
||||
Property = 7,
|
||||
Field = 8,
|
||||
Constructor = 9,
|
||||
Enum = 10,
|
||||
Interface = 11,
|
||||
Function = 12,
|
||||
Variable = 13,
|
||||
Constant = 14,
|
||||
String = 15,
|
||||
Number = 16,
|
||||
Boolean = 17,
|
||||
Array = 18,
|
||||
}
|
||||
|
||||
public class SymbolInformation
|
||||
{
|
||||
public string Name { get; set; }
|
||||
|
||||
public SymbolKind Kind { get; set; }
|
||||
|
||||
public Location Location { get; set; }
|
||||
|
||||
public string ContainerName { get; set;}
|
||||
}
|
||||
|
||||
public class DocumentSymbolRequest
|
||||
{
|
||||
public static readonly
|
||||
RequestType<TextDocumentIdentifier, SymbolInformation[]> Type =
|
||||
RequestType<TextDocumentIdentifier, SymbolInformation[]>.Create("textDocument/documentSymbol");
|
||||
}
|
||||
|
||||
public class WorkspaceSymbolRequest
|
||||
{
|
||||
public static readonly
|
||||
RequestType<WorkspaceSymbolParams, SymbolInformation[]> Type =
|
||||
RequestType<WorkspaceSymbolParams, SymbolInformation[]>.Create("workspace/symbol");
|
||||
}
|
||||
|
||||
public class WorkspaceSymbolParams
|
||||
{
|
||||
public string Query { get; set;}
|
||||
}
|
||||
}
|
||||
|
||||
248
src/Microsoft.SqlTools.ServiceLayer/Workspace/Workspace.cs
Normal file
248
src/Microsoft.SqlTools.ServiceLayer/Workspace/Workspace.cs
Normal file
@@ -0,0 +1,248 @@
|
||||
//
|
||||
// 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.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Linq;
|
||||
using Microsoft.SqlTools.EditorServices.Utility;
|
||||
using Microsoft.SqlTools.ServiceLayer.Workspace.Contracts;
|
||||
|
||||
namespace Microsoft.SqlTools.ServiceLayer.Workspace
|
||||
{
|
||||
/// <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 : IDisposable
|
||||
{
|
||||
#region Private Fields
|
||||
|
||||
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>
|
||||
public Workspace()
|
||||
{
|
||||
}
|
||||
|
||||
#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.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, @"`(?=[ \[\]])", "");
|
||||
}
|
||||
|
||||
/// <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.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);
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
#region IDisposable Implementation
|
||||
|
||||
/// <summary>
|
||||
/// Disposes of any Runspaces that were created for the
|
||||
/// services used in this session.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
//
|
||||
// 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.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.SqlTools.EditorServices.Utility;
|
||||
using Microsoft.SqlTools.ServiceLayer.Hosting;
|
||||
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol;
|
||||
using Microsoft.SqlTools.ServiceLayer.Workspace.Contracts;
|
||||
|
||||
namespace Microsoft.SqlTools.ServiceLayer.Workspace
|
||||
{
|
||||
/// <summary>
|
||||
/// Class for handling requests/events that deal with the state of the workspace, including the
|
||||
/// opening and closing of files, the changing of configuration, etc.
|
||||
/// </summary>
|
||||
/// <typeparam name="TConfig">
|
||||
/// The type of the class used for serializing and deserializing the configuration. Must be the
|
||||
/// actual type of the instance otherwise deserialization will be incomplete.
|
||||
/// </typeparam>
|
||||
public class WorkspaceService<TConfig> where TConfig : class, new()
|
||||
{
|
||||
|
||||
#region Singleton Instance Implementation
|
||||
|
||||
private static readonly Lazy<WorkspaceService<TConfig>> instance = new Lazy<WorkspaceService<TConfig>>(() => new WorkspaceService<TConfig>());
|
||||
|
||||
public static WorkspaceService<TConfig> Instance
|
||||
{
|
||||
get { return instance.Value; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Default, parameterless constructor.
|
||||
/// TODO: Figure out how to make this truely singleton even with dependency injection for tests
|
||||
/// </summary>
|
||||
public WorkspaceService()
|
||||
{
|
||||
ConfigChangeCallbacks = new List<ConfigChangeCallback>();
|
||||
TextDocChangeCallbacks = new List<TextDocChangeCallback>();
|
||||
TextDocOpenCallbacks = new List<TextDocOpenCallback>();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Properties
|
||||
|
||||
public Workspace Workspace { get; private set; }
|
||||
|
||||
public TConfig CurrentSettings { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Delegate for callbacks that occur when the configuration for the workspace changes
|
||||
/// </summary>
|
||||
/// <param name="newSettings">The settings that were just set</param>
|
||||
/// <param name="oldSettings">The settings before they were changed</param>
|
||||
/// <param name="eventContext">Context of the event that triggered the callback</param>
|
||||
/// <returns></returns>
|
||||
public delegate Task ConfigChangeCallback(TConfig newSettings, TConfig oldSettings, EventContext eventContext);
|
||||
|
||||
/// <summary>
|
||||
/// Delegate for callbacks that occur when the current text document changes
|
||||
/// </summary>
|
||||
/// <param name="changedFiles">Array of files that changed</param>
|
||||
/// <param name="eventContext">Context of the event raised for the changed files</param>
|
||||
public delegate Task TextDocChangeCallback(ScriptFile[] changedFiles, EventContext eventContext);
|
||||
|
||||
/// <summary>
|
||||
/// Delegate for callbacks that occur when a text document is opened
|
||||
/// </summary>
|
||||
/// <param name="openFile">File that was opened</param>
|
||||
/// <param name="eventContext">Context of the event raised for the changed files</param>
|
||||
public delegate Task TextDocOpenCallback(ScriptFile openFile, EventContext eventContext);
|
||||
|
||||
/// <summary>
|
||||
/// List of callbacks to call when the configuration of the workspace changes
|
||||
/// </summary>
|
||||
private List<ConfigChangeCallback> ConfigChangeCallbacks { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// List of callbacks to call when the current text document changes
|
||||
/// </summary>
|
||||
private List<TextDocChangeCallback> TextDocChangeCallbacks { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// List of callbacks to call when a text document is opened
|
||||
/// </summary>
|
||||
private List<TextDocOpenCallback> TextDocOpenCallbacks { get; set; }
|
||||
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public Methods
|
||||
|
||||
public void InitializeService(ServiceHost serviceHost)
|
||||
{
|
||||
// Create a workspace that will handle state for the session
|
||||
Workspace = new Workspace();
|
||||
CurrentSettings = new TConfig();
|
||||
|
||||
// Register the handlers for when changes to the workspae occur
|
||||
serviceHost.SetEventHandler(DidChangeTextDocumentNotification.Type, HandleDidChangeTextDocumentNotification);
|
||||
serviceHost.SetEventHandler(DidOpenTextDocumentNotification.Type, HandleDidOpenTextDocumentNotification);
|
||||
serviceHost.SetEventHandler(DidCloseTextDocumentNotification.Type, HandleDidCloseTextDocumentNotification);
|
||||
serviceHost.SetEventHandler(DidChangeConfigurationNotification<TConfig>.Type, HandleDidChangeConfigurationNotification);
|
||||
|
||||
// Register an initialization handler that sets the workspace path
|
||||
serviceHost.RegisterInitializeTask(async (parameters, contect) =>
|
||||
{
|
||||
Logger.Write(LogLevel.Verbose, "Initializing workspace service");
|
||||
|
||||
if (Workspace != null)
|
||||
{
|
||||
Workspace.WorkspacePath = parameters.RootPath;
|
||||
}
|
||||
await Task.FromResult(0);
|
||||
});
|
||||
|
||||
// Register a shutdown request that disposes the workspace
|
||||
serviceHost.RegisterShutdownTask(async (parameters, context) =>
|
||||
{
|
||||
Logger.Write(LogLevel.Verbose, "Shutting down workspace service");
|
||||
|
||||
if (Workspace != null)
|
||||
{
|
||||
Workspace.Dispose();
|
||||
Workspace = null;
|
||||
}
|
||||
await Task.FromResult(0);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a new task to be called when the configuration has been changed. Use this to
|
||||
/// handle changing configuration and changing the current configuration.
|
||||
/// </summary>
|
||||
/// <param name="task">Task to handle the request</param>
|
||||
public void RegisterConfigChangeCallback(ConfigChangeCallback task)
|
||||
{
|
||||
ConfigChangeCallbacks.Add(task);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a new task to be called when the text of a document changes.
|
||||
/// </summary>
|
||||
/// <param name="task">Delegate to call when the document changes</param>
|
||||
public void RegisterTextDocChangeCallback(TextDocChangeCallback task)
|
||||
{
|
||||
TextDocChangeCallbacks.Add(task);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a new task to be called when a file is opened
|
||||
/// </summary>
|
||||
/// <param name="task">Delegate to call when a document is opened</param>
|
||||
public void RegisterTextDocOpenCallback(TextDocOpenCallback task)
|
||||
{
|
||||
TextDocOpenCallbacks.Add(task);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Event Handlers
|
||||
|
||||
/// <summary>
|
||||
/// Handles text document change events
|
||||
/// </summary>
|
||||
protected Task HandleDidChangeTextDocumentNotification(
|
||||
DidChangeTextDocumentParams textChangeParams,
|
||||
EventContext eventContext)
|
||||
{
|
||||
StringBuilder msg = new StringBuilder();
|
||||
msg.Append("HandleDidChangeTextDocumentNotification");
|
||||
List<ScriptFile> changedFiles = new List<ScriptFile>();
|
||||
|
||||
// A text change notification can batch multiple change requests
|
||||
foreach (var textChange in textChangeParams.ContentChanges)
|
||||
{
|
||||
string fileUri = textChangeParams.Uri ?? textChangeParams.TextDocument.Uri;
|
||||
msg.AppendLine(String.Format(" File: {0}", fileUri));
|
||||
|
||||
ScriptFile changedFile = Workspace.GetFile(fileUri);
|
||||
|
||||
changedFile.ApplyChange(
|
||||
GetFileChangeDetails(
|
||||
textChange.Range.Value,
|
||||
textChange.Text));
|
||||
|
||||
changedFiles.Add(changedFile);
|
||||
}
|
||||
|
||||
Logger.Write(LogLevel.Verbose, msg.ToString());
|
||||
|
||||
var handlers = TextDocChangeCallbacks.Select(t => t(changedFiles.ToArray(), eventContext));
|
||||
return Task.WhenAll(handlers);
|
||||
}
|
||||
|
||||
protected async Task HandleDidOpenTextDocumentNotification(
|
||||
DidOpenTextDocumentNotification openParams,
|
||||
EventContext eventContext)
|
||||
{
|
||||
Logger.Write(LogLevel.Verbose, "HandleDidOpenTextDocumentNotification");
|
||||
|
||||
// read the SQL file contents into the ScriptFile
|
||||
ScriptFile openedFile = Workspace.GetFileBuffer(openParams.Uri, openParams.Text);
|
||||
|
||||
// Propagate the changes to the event handlers
|
||||
var textDocOpenTasks = TextDocOpenCallbacks.Select(
|
||||
t => t(openedFile, eventContext));
|
||||
|
||||
await Task.WhenAll(textDocOpenTasks);
|
||||
}
|
||||
|
||||
protected Task HandleDidCloseTextDocumentNotification(
|
||||
TextDocumentIdentifier closeParams,
|
||||
EventContext eventContext)
|
||||
{
|
||||
Logger.Write(LogLevel.Verbose, "HandleDidCloseTextDocumentNotification");
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles the configuration change event
|
||||
/// </summary>
|
||||
protected async Task HandleDidChangeConfigurationNotification(
|
||||
DidChangeConfigurationParams<TConfig> configChangeParams,
|
||||
EventContext eventContext)
|
||||
{
|
||||
Logger.Write(LogLevel.Verbose, "HandleDidChangeConfigurationNotification");
|
||||
|
||||
// Propagate the changes to the event handlers
|
||||
var configUpdateTasks = ConfigChangeCallbacks.Select(
|
||||
t => t(configChangeParams.Settings, CurrentSettings, eventContext));
|
||||
await Task.WhenAll(configUpdateTasks);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Private Helpers
|
||||
|
||||
/// <summary>
|
||||
/// Switch from 0-based offsets to 1 based offsets
|
||||
/// </summary>
|
||||
/// <param name="changeRange"></param>
|
||||
/// <param name="insertString"></param>
|
||||
private static FileChange GetFileChangeDetails(Range changeRange, string insertString)
|
||||
{
|
||||
// The protocol's positions are zero-based so add 1 to all offsets
|
||||
return new FileChange
|
||||
{
|
||||
InsertString = insertString,
|
||||
Line = changeRange.Start.Line + 1,
|
||||
Offset = changeRange.Start.Character + 1,
|
||||
EndLine = changeRange.End.Line + 1,
|
||||
EndOffset = changeRange.End.Character + 1
|
||||
};
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user