Added new Kusto ServiceLayer (#1009)

* Copy smoModel some rename

* Copy entire service layer

* Building copy

* Fixing some references

* Launch profile

* Resolve namespace issues

* Compiling tests. Correct manifest.

* Fixing localization resources

* ReliableKustoClient

* Some trimming of extra code and Kusto code

* Kusto client creation in bindingContent

* Removing Smo and new Kusto classes

* More trimming

* Kusto schema hookup

* Solidying DataSource abstraction

* Solidifying further

* Latest refatoring

* More refactoring

* Building and launching Kusto service layer

* Working model which enumerates databases

* Refactoring to pass IDataSource to all tree nodes

* Removing some dependencies on the context

* Working with tables and schema

* Comment checkin

* Refactoring to give out select script

* Query created and sent back to ADS

* Fix query generation

* Fix listing of databases

* Tunneling the query through.

* Successful query execution

* Return only results table

* Deleting Cms

* Delete DacFx

* Delete SchemaCompare and TaskServices

* Change build definition to not stop at launch

* Fix error after merge

* Save Kusto results in different formats (#935)

* save results as csv etc

* some fixes

Co-authored-by: Monica Gupta <mogupt@microsoft.com>

* 2407 Added OrderBy clause in KustoDataSource > GetDatabaseMetaData and GetColumnMetadata (#959)

* 2405 Defaulted Options when setting ServerInfo in ConnectionService > GetConnectionCompleteParams (#965)

* 2747 Fixed IsUnknownType error for Kusto (#989)

* 2747 Removed unused directives in Kusto > DbColumnWrapper. Refactored IsUnknownType to handle null DataTypeName

* 2747 Reverted IsUnknownType change in DbColumnWrapper. Changed DataTypeName to get calue from ColumnType. Refactored SafeGetValue to type check before hard casting to reduce case exceptions.

* Added EmbeddedResourceUseDependentUponConvention to Microsoft.Kusto.ServiceLayer.csproj. Also renamed DACfx to match Microsoft.SqlTools.ServiceLayer. Added to compile Exclude="**/obj/**/*.cs"

* Srahman cleanup sql code (#992)

* Removed Management and Security Service Code.

* Remove FileBrowser service

* Comment why we are using SqlServer library

* Remove SQL specific type definitions

* clean up formatter service (#996)

Co-authored-by: Monica Gupta <mogupt@microsoft.com>

* Code clean up and Kusto intellisense (#994)

* Code clean up and Kusto intellisense

* Addressed few comments

* Addressed few comments

* addressed comments

Co-authored-by: Monica Gupta <mogupt@microsoft.com>

* Return multiple tables for Kusto

* Changes required for Kusto manage dashboard (#1039)

* Changes required for manage dashboard

* Addressed comments

Co-authored-by: Monica Gupta <mogupt@microsoft.com>

* 2728 Kusto function support (#1038)

* loc update (#914)

* loc update

* loc updates

* 2728 moved ColumnInfo and KustoResultsReader to separate files. Added Folder and Function to TreeNode.cs

* 2728 Added FunctionInfo. Added Folder to ColumnInfo. Removed partial class from KustoResultsReader. Set Function.IsAlwaysLeaf=true in TreeNode.cs. In KustoDataSource changed tableMetadata type to TableMetaData. Added folder and function dictionaries. Refactored GetSchema function. Renamed GenerateColumnMetadataKey to GenerateMetadataKey

* 2728 Added FunctionInfo. Added Folder to ColumnInfo. Removed partial class from KustoResultsReader. Set Function.IsAlwaysLeaf=true in TreeNode.cs. In KustoDataSource changed tableMetadata type to TableMetaData. Added folder and function dictionaries. Refactored GetSchema function. Renamed GenerateColumnMetadataKey to GenerateMetadataKey

* 2728 Created new SqlConnection within using block. Refactored KustoDataSource > columnmetadata to sort on get instead of insert.

* 2728 Added GetFunctionInfo function to KustoDataSource.

* 2728 Reverted change to Microsoft.Kusto.ServiceLayer.csproj from merge

* 2728 Reverted change to SqlTools.ServiceLayer\Localization\transXliff

* 2728 Reverted change to sr.de.xlf and sr.zh-hans.xlf

* 2728 Refactored KustoDataSource Function folders to support subfolders

* 2728 Refactored KustoDataSource to use urn for folders, functions, and tables instead of name.

* Merge remote-tracking branch 'origin/main' into feature-ADE

# Conflicts:
#	Packages.props

* 2728 Moved metadata files into Metadata subdirectory. Added GenerateAlterFunction to IDataSource and DataSourceBase.

* 2728 Added summary information to SafeAdd in SystemExtensions. Renamed local variable in SetTableMetadata

* 2728 Moved SafeAdd from SystemExtensions to KustoQueryUtils. Added check when getting database schema to return existing records before querying again. Added AddRange function to KustoQueryUtils. Created SetFolderMetadataForFunctions method.

* 2728 Added DatabaseKeyPrefix to only return tables to a database for the dashboard. Added logic to store all database tables within the tableMetadata dictionary for the dashboard.

* 2728 Created TableInfo and moved info objects into Models directory. Refactored KustoDataSource to lazy load columns for tables. Refactored logic to load tables using cslschema instead of schema.

* 2728 Renamed LoadColumnSchema to GetTableSchema to be consistent.

Co-authored-by: khoiph1 <khoiph@microsoft.com>

* Addressed comments

Co-authored-by: Shafiq Rahman <srahman@microsoft.com>
Co-authored-by: Monica Gupta <mogupt@microsoft.com>
Co-authored-by: Justin M <63619224+JustinMDotNet@users.noreply.github.com>
Co-authored-by: rkselfhost <rkselfhost@outlook.com>
Co-authored-by: khoiph1 <khoiph@microsoft.com>
This commit is contained in:
Monica Gupta
2020-08-12 15:34:38 -07:00
committed by GitHub
parent d2f5bfaa16
commit 148b6e398d
276 changed files with 75983 additions and 1 deletions

View File

@@ -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.Kusto.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
}
}

View File

@@ -0,0 +1,120 @@
//
// 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.Kusto.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(SR.WorkspaceServiceBufferPositionOutOfOrder(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,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.Hosting.Protocol.Contracts;
namespace Microsoft.Kusto.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; }
}
}

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.Kusto.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; }
}
}

View File

@@ -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.Kusto.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 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[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(scriptFile, 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[Line - 1];
return new FilePosition(scriptFile, Line, scriptLine.Length + 1);
}
#endregion
}
}

View File

@@ -0,0 +1,450 @@
//
// 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.Utility;
namespace Microsoft.Kusto.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 ClientUri property.
/// </summary>
public string Id
{
get { return this.ClientUri.ToLower(); }
}
/// <summary>
/// Gets the path at which this file resides.
/// </summary>
public string FilePath { get; private set; }
/// <summary>
/// Gets or sets the URI which the editor client uses to identify this file.
/// Setter for testing purposes only
/// virtual to allow mocking.
/// </summary>
public virtual string ClientUri { get; internal 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 or sets a string containing the full contents of the file.
/// Setter for testing purposes only
/// </summary>
public virtual string Contents
{
get
{
return string.Join("\r\n", FileLines);
}
set
{
FileLines = value != null ? value.Split('\n') : null;
}
}
/// <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;
}
#endregion
#region Constructors
/// <summary>
/// Add a default constructor for testing
/// </summary>
public ScriptFile()
{
ClientUri = "test.sql";
}
/// <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="clientUri">The URI 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 clientUri,
TextReader textReader)
{
FilePath = filePath;
ClientUri = clientUri;
IsAnalysisEnabled = true;
IsInMemory = Workspace.IsPathInMemoryOrNonFileUri(filePath);
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="clientUri">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 clientUri,
string initialBuffer)
{
FilePath = filePath;
ClientUri = clientUri;
IsAnalysisEnabled = true;
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, FileLines.Count + 1);
return FileLines[lineNumber - 1];
}
/// <summary>
/// Gets the text under a specific range
/// </summary>
public string GetTextInRange(BufferRange range)
{
return string.Join(Environment.NewLine, GetLinesInRange(range));
}
/// <summary>
/// Gets a range of lines from the file's contents. Virtual method to allow for
/// mocking.
/// </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 virtual string[] GetLinesInRange(BufferRange bufferRange)
{
ValidatePosition(bufferRange.Start);
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 = 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)
{
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 > 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));
}
}
/// <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)
{
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++;
}
}
/// <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, 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;
}
/// <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;
ValidatePosition(newLine, newColumn);
string scriptLine = 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 < 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);
}
/// <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.
FileLines =
fileContents
.Split('\n')
.Select(line => line.TrimEnd('\r'))
.ToList();
}
#endregion
}
}

View File

@@ -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.Kusto.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
}
}

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.Kusto.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
}
}

View File

@@ -0,0 +1,295 @@
//
// 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.Hosting.Protocol.Contracts;
namespace Microsoft.Kusto.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
{
/// <summary>
/// Gets or sets the document identifier.
/// </summary>
public TextDocumentIdentifier TextDocument { get; set; }
/// <summary>
/// Gets or sets the position in the document.
/// </summary>
public Position Position { get; set; }
}
/// <summary>
/// Defines a text document.
/// </summary>
[DebuggerDisplay("TextDocumentItem = {Uri}")]
public class TextDocumentItem
{
/// <summary>
/// Gets or sets the URI which identifies the path of the
/// text document.
/// </summary>
public string Uri { get; set; }
/// <summary>
/// Gets or sets the language of the document
/// </summary>
public string LanguageId { get; set; }
/// <summary>
/// Gets or sets the version of the document
/// </summary>
public int Version { get; set; }
/// <summary>
/// Gets or sets the full content of the document.
/// </summary>
public string Text { get; set; }
}
public class DidOpenTextDocumentNotification
{
public static readonly
EventType<DidOpenTextDocumentNotification> Type =
EventType<DidOpenTextDocumentNotification>.Create("textDocument/didOpen");
/// <summary>
/// Gets or sets the opened document.
/// </summary>
public TextDocumentItem TextDocument { get; set; }
}
public class DidCloseTextDocumentNotification
{
public static readonly
EventType<DidCloseTextDocumentParams> Type =
EventType<DidCloseTextDocumentParams>.Create("textDocument/didClose");
}
public class DidChangeTextDocumentNotification
{
public static readonly
EventType<DidChangeTextDocumentParams> Type =
EventType<DidChangeTextDocumentParams>.Create("textDocument/didChange");
}
public class DidCloseTextDocumentParams
{
/// <summary>
/// Gets or sets the closed document.
/// </summary>
public TextDocumentItem TextDocument { get; set; }
}
public class DidChangeTextDocumentParams
{
/// <summary>
/// Gets or sets the changed document.
/// </summary>
public VersionedTextDocumentIdentifier TextDocument { get; set; }
/// <summary>
/// Gets or sets the list of changes to the document content.
/// </summary>
public TextDocumentChangeEvent[] ContentChanges { get; set; }
}
/// <summary>
/// Define a specific version of a text document
/// </summary>
public class VersionedTextDocumentIdentifier : TextDocumentIdentifier
{
/// <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; }
/// <summary>
/// Overrides the base equality method
/// </summary>
/// <param name="obj"></param>
/// <returns></returns>
public override bool Equals(object obj)
{
if (obj == null || (obj as Position == null))
{
return false;
}
Position p = (Position) obj;
bool result = (Line == p.Line) && (Character == p.Character);
return result;
}
/// <summary>
/// Overrides the base GetHashCode method
/// </summary>
/// <returns></returns>
public override int GetHashCode()
{
int hash = 17;
hash = hash * 23 + Line.GetHashCode();
hash = hash * 23 + Character.GetHashCode();
return hash;
}
}
[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; }
/// <summary>
/// Overrides the base equality method
/// </summary>
/// <param name="obj"></param>
/// <returns></returns>
public override bool Equals(object obj)
{
if (obj == null || !(obj is Range))
{
return false;
}
Range range = (Range) obj;
bool sameStart = range.Start.Equals(Start);
bool sameEnd = range.End.Equals(End);
return (sameStart && sameEnd);
}
/// <summary>
/// Overrides the base GetHashCode method
/// </summary>
/// <returns></returns>
public override int GetHashCode()
{
int hash = 17;
hash = hash * 23 + Start.GetHashCode();
hash = hash * 23 + End.GetHashCode();
return hash;
}
}
[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; }
/// <summary>
/// Overrides the base equality method
/// </summary>
/// <param name="obj"></param>
/// <returns></returns>
public override bool Equals(object obj)
{
if (obj == null || (obj as Location == null))
{
return false;
}
Location loc = (Location)obj;
bool sameUri = string.Equals(loc.Uri, Uri);
bool sameRange = loc.Range.Equals(Range);
return (sameUri && sameRange);
}
/// <summary>
/// Overrides the base GetHashCode method
/// </summary>
/// <returns></returns>
public override int GetHashCode()
{
int hash = 17;
hash = hash * 23 + Uri.GetHashCode();
hash = hash * 23 + Range.GetHashCode();
return hash;
}
}
public enum FileChangeType
{
Created = 1,
Changed,
Deleted
}
public class FileEvent
{
public string Uri { get; set; }
public FileChangeType Type { get; set; }
}
}

View File

@@ -0,0 +1,70 @@
//
// 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.Hosting.Protocol.Contracts;
namespace Microsoft.Kusto.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<DocumentSymbolParams, SymbolInformation[]> Type =
RequestType<DocumentSymbolParams, SymbolInformation[]>.Create("textDocument/documentSymbol");
}
/// <summary>
/// Defines a set of parameters to send document symbol request
/// </summary>
public class DocumentSymbolParams
{
public TextDocumentIdentifier TextDocument { get; set; }
}
public class WorkspaceSymbolRequest
{
public static readonly
RequestType<WorkspaceSymbolParams, SymbolInformation[]> Type =
RequestType<WorkspaceSymbolParams, SymbolInformation[]>.Create("workspace/symbol");
}
public class WorkspaceSymbolParams
{
public string Query { get; set;}
}
}

View File

@@ -0,0 +1,365 @@
//
// 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.Utility;
using Microsoft.Kusto.ServiceLayer.Workspace.Contracts;
using System.Runtime.InteropServices;
using Microsoft.Kusto.ServiceLayer.Utility;
using System.Diagnostics;
namespace Microsoft.Kusto.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 const string UntitledScheme = "untitled";
private static readonly HashSet<string> fileUriSchemes = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"file",
UntitledScheme,
"tsqloutput"
};
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>
/// Checks if a given URI is contained in a workspace
/// </summary>
/// <param name="filePath"></param>
/// <returns>Flag indicating if the file is tracked in workspace</returns>
public bool ContainsFile(string filePath)
{
Validate.IsNotNullOrWhitespaceString("filePath", filePath);
// Resolve the full file path
ResolvedFile resolvedFile = this.ResolveFilePath(filePath);
string keyName = resolvedFile.LowercaseClientUri;
ScriptFile scriptFile = null;
return this.workspaceFiles.TryGetValue(keyName, out scriptFile);
}
/// <summary>
/// Gets an open file in the workspace. If the file isn't open but
/// exists on the filesystem, load and return it. Virtual method to
/// allow for mocking
/// </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 virtual ScriptFile GetFile(string filePath)
{
Validate.IsNotNullOrWhitespaceString("filePath", filePath);
if (IsNonFileUri(filePath))
{
return null;
}
// Resolve the full file path
ResolvedFile resolvedFile = this.ResolveFilePath(filePath);
string keyName = resolvedFile.LowercaseClientUri;
// Make sure the file isn't already loaded into the workspace
ScriptFile scriptFile = null;
if (!this.workspaceFiles.TryGetValue(keyName, out scriptFile))
{
if (IsUntitled(resolvedFile.FilePath)
|| !resolvedFile.CanReadFromDisk
|| !File.Exists(resolvedFile.FilePath))
{
// It's either not a registered untitled file, or not a valid file on disk
// so any attempt to read from disk will fail.
return null;
}
// This method allows FileNotFoundException to bubble up
// if the file isn't found.
using (FileStream fileStream = new FileStream(resolvedFile.FilePath, FileMode.Open, FileAccess.Read))
using (StreamReader streamReader = new StreamReader(fileStream, Encoding.UTF8))
{
scriptFile = new ScriptFile(resolvedFile.FilePath, resolvedFile.ClientUri,streamReader);
this.workspaceFiles.Add(keyName, scriptFile);
}
Logger.Write(TraceEventType.Verbose, "Opened file on disk: " + resolvedFile.FilePath);
}
return scriptFile;
}
/// <summary>
/// Resolves a URI identifier into an actual file on disk if it exists.
/// </summary>
/// <param name="clientUri">The URI identifying the file</param>
/// <returns></returns>
private ResolvedFile ResolveFilePath(string clientUri)
{
bool canReadFromDisk = false;
string filePath = clientUri;
if (!IsPathInMemoryOrNonFileUri(clientUri))
{
if (clientUri.StartsWith(@"file://"))
{
// VS Code encodes the ':' character in the drive name, which can lead to problems parsing
// the URI, so unencode it if present. See https://github.com/Microsoft/vscode/issues/2990
clientUri = clientUri.Replace("%3A/", ":/", StringComparison.OrdinalIgnoreCase);
// Client sent the path in URI format, extract the local path and trim
// any extraneous slashes
Uri fileUri = new Uri(clientUri);
filePath = fileUri.LocalPath;
if (filePath.StartsWith("//") || filePath.StartsWith("\\\\") || filePath.StartsWith("/"))
{
filePath = filePath.Substring(1);
}
}
// 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);
// Client paths are handled a bit differently because of how we currently identifiers in
// ADS. The URI is passed around as an identifier - but for things we control like connecting
// an editor the URI we pass in is NOT escaped fully. This is a problem for certain functionality
// which is handled by VS Code - such as Intellise Completion - as the URI passed in there is
// the fully escaped URI. That means we need to do some extra work to make sure that the URI values
// are consistent.
// So to solve that we'll make sure to unescape ALL uri's that are passed in and store that value for
// use as an identifier (filePath will be the actual file path on disk).
// # and ? are still always escaped though by ADS so we need to escape those again to get them to actually
// match
clientUri = Uri.UnescapeDataString(UnescapePath(clientUri));
clientUri = clientUri.Replace("#", "%23");
clientUri = clientUri.Replace("?", "%3F");
// switch to unix path separators on non-Windows platforms
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
filePath = filePath.Replace('\\', '/');
clientUri = clientUri.Replace('\\', '/');
}
// Get the absolute file path
ResolvedFile resolvedFile = FileUtilities.TryGetFullPath(filePath, clientUri);
filePath = resolvedFile.FilePath;
canReadFromDisk = resolvedFile.CanReadFromDisk;
}
Logger.Write(TraceEventType.Verbose, "Resolved path: " + clientUri);
return new ResolvedFile(filePath, clientUri, canReadFromDisk);
}
/// <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.IsNotNullOrWhitespaceString("filePath", filePath);
if (IsNonFileUri(filePath))
{
return null;
}
// Resolve the full file path
ResolvedFile resolvedFile = this.ResolveFilePath(filePath);
string keyName = resolvedFile.LowercaseClientUri;
// Make sure the file isn't already loaded into the workspace
ScriptFile scriptFile = null;
if (!this.workspaceFiles.TryGetValue(keyName, out scriptFile))
{
scriptFile = new ScriptFile(resolvedFile.FilePath, resolvedFile.ClientUri, initialBuffer);
this.workspaceFiles.Add(keyName, scriptFile);
Logger.Write(TraceEventType.Verbose, "Opened file as in-memory buffer: " + resolvedFile.FilePath);
}
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);
}
internal string GetBaseFilePath(string filePath)
{
if (IsPathInMemoryOrNonFileUri(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);
}
internal 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;
}
internal static bool IsPathInMemoryOrNonFileUri(string path)
{
string scheme = GetScheme(path);
if (!string.IsNullOrEmpty(scheme))
{
return !scheme.Equals("file");
}
return false;
}
public static string GetScheme(string uri)
{
string windowsFilePattern = @"^(?:[\w]\:|\\)";
if (Regex.IsMatch(uri, windowsFilePattern))
{
// Handle windows paths, these conflict with other "URI" handling
return null;
}
// Match anything that starts with xyz:, as VSCode send URIs in the format untitled:, git: etc.
string pattern = "^([a-z][a-z0-9+.-]*):";
Match match = Regex.Match(uri, pattern);
if (match != null && match.Success)
{
return match.Groups[1].Value;
}
return null;
}
private bool IsNonFileUri(string path)
{
string scheme = GetScheme(path);
if (!string.IsNullOrEmpty(scheme))
{
return !fileUriSchemes.Contains(scheme); ;
}
return false;
}
private bool IsUntitled(string path)
{
string scheme = GetScheme(path);
if (scheme != null && scheme.Length > 0)
{
return string.Compare(UntitledScheme, scheme, StringComparison.OrdinalIgnoreCase) == 0;
}
return false;
}
#endregion
#region IDisposable Implementation
/// <summary>
/// Disposes of any Runspaces that were created for the
/// services used in this session.
/// </summary>
public void Dispose()
{
}
#endregion
}
}

View File

@@ -0,0 +1,373 @@
//
// 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.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.SqlTools.Hosting.Protocol;
using Microsoft.Kusto.ServiceLayer.Hosting;
using Microsoft.Kusto.ServiceLayer.Workspace.Contracts;
using Microsoft.SqlTools.Utility;
using Range = Microsoft.Kusto.ServiceLayer.Workspace.Contracts.Range;
namespace Microsoft.Kusto.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 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>();
TextDocCloseCallbacks = new List<TextDocCloseCallback>();
CurrentSettings = new TConfig();
}
#endregion
#region Properties
/// <summary>
/// Workspace object for the service. Virtual to allow for mocking
/// </summary>
public virtual Workspace Workspace { get; internal set; }
/// <summary>
/// Current settings for the workspace
/// </summary>
public TConfig CurrentSettings { get; internal 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="uri">Request uri</param>
/// <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(string uri, ScriptFile openFile, EventContext eventContext);
/// <summary>
/// Delegate for callbacks that occur when a text document is closed
/// </summary>
/// <param name="uri">Request uri</param>
/// <param name="closedFile">File that was closed</param>
/// <param name="eventContext">Context of the event raised for changed files</param>
public delegate Task TextDocCloseCallback(string uri, ScriptFile closedFile, 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; }
/// <summary>
/// List of callbacks to call when a text document is closed
/// </summary>
private List<TextDocCloseCallback> TextDocCloseCallbacks { get; set; }
#endregion
#region Public Methods
public void InitializeService(ServiceHost serviceHost)
{
// Create a workspace that will handle state for the session
Workspace = new Workspace();
// 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(TraceEventType.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(TraceEventType.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 text document closes.
/// </summary>
/// <param name="task">Delegate to call when the document closes</param>
public void RegisterTextDocCloseCallback(TextDocCloseCallback task)
{
TextDocCloseCallbacks.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>
internal Task HandleDidChangeTextDocumentNotification(
DidChangeTextDocumentParams textChangeParams,
EventContext eventContext)
{
try
{
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.TextDocument.Uri ?? textChangeParams.TextDocument.Uri;
msg.AppendLine(string.Format(" File: {0}", fileUri));
ScriptFile changedFile = Workspace.GetFile(fileUri);
if (changedFile != null)
{
changedFile.ApplyChange(
GetFileChangeDetails(
textChange.Range.Value,
textChange.Text));
changedFiles.Add(changedFile);
}
}
Logger.Write(TraceEventType.Verbose, msg.ToString());
var handlers = TextDocChangeCallbacks.Select(t => t(changedFiles.ToArray(), eventContext));
return Task.WhenAll(handlers);
}
catch (Exception ex)
{
Logger.Write(TraceEventType.Error, "Unknown error " + ex.ToString());
// Swallow exceptions here to prevent us from crashing
// TODO: this probably means the ScriptFile model is in a bad state or out of sync with the actual file; we should recover here
return Task.FromResult(true);
}
}
internal async Task HandleDidOpenTextDocumentNotification(
DidOpenTextDocumentNotification openParams,
EventContext eventContext)
{
try
{
Logger.Write(TraceEventType.Verbose, "HandleDidOpenTextDocumentNotification");
if (IsScmEvent(openParams.TextDocument.Uri))
{
return;
}
// read the SQL file contents into the ScriptFile
ScriptFile openedFile = Workspace.GetFileBuffer(openParams.TextDocument.Uri, openParams.TextDocument.Text);
if (openedFile == null)
{
return;
}
// Propagate the changes to the event handlers
var textDocOpenTasks = TextDocOpenCallbacks.Select(
t => t(openParams.TextDocument.Uri, openedFile, eventContext));
await Task.WhenAll(textDocOpenTasks);
}
catch (Exception ex)
{
Logger.Write(TraceEventType.Error, "Unknown error " + ex.ToString());
// Swallow exceptions here to prevent us from crashing
// TODO: this probably means the ScriptFile model is in a bad state or out of sync with the actual file; we should recover here
return;
}
}
internal async Task HandleDidCloseTextDocumentNotification(
DidCloseTextDocumentParams closeParams,
EventContext eventContext)
{
try
{
Logger.Write(TraceEventType.Verbose, "HandleDidCloseTextDocumentNotification");
if (IsScmEvent(closeParams.TextDocument.Uri))
{
return;
}
// Skip closing this file if the file doesn't exist
var closedFile = Workspace.GetFile(closeParams.TextDocument.Uri);
if (closedFile == null)
{
return;
}
// Trash the existing document from our mapping
Workspace.CloseFile(closedFile);
// Send out a notification to other services that have subscribed to this event
var textDocClosedTasks = TextDocCloseCallbacks.Select(t => t(closeParams.TextDocument.Uri, closedFile, eventContext));
await Task.WhenAll(textDocClosedTasks);
}
catch (Exception ex)
{
Logger.Write(TraceEventType.Error, "Unknown error " + ex.ToString());
// Swallow exceptions here to prevent us from crashing
// TODO: this probably means the ScriptFile model is in a bad state or out of sync with the actual file; we should recover here
return;
}
}
/// <summary>
/// Handles the configuration change event
/// </summary>
internal async Task HandleDidChangeConfigurationNotification(
DidChangeConfigurationParams<TConfig> configChangeParams,
EventContext eventContext)
{
try
{
Logger.Write(TraceEventType.Verbose, "HandleDidChangeConfigurationNotification");
// Propagate the changes to the event handlers
var configUpdateTasks = ConfigChangeCallbacks.Select(
t => t(configChangeParams.Settings, CurrentSettings, eventContext));
await Task.WhenAll(configUpdateTasks);
}
catch (Exception ex)
{
Logger.Write(TraceEventType.Error, "Unknown error " + ex.ToString());
// Swallow exceptions here to prevent us from crashing
// TODO: this probably means the ScriptFile model is in a bad state or out of sync with the actual file; we should recover here
return;
}
}
#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
};
}
internal static bool IsScmEvent(string filePath)
{
// if the URI is prefixed with git: then we want to skip processing that file
return filePath.StartsWith("git:");
}
#endregion
}
}