Skip to content

Fix up extension API #2053

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Aug 22, 2023
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions src/PowerShellEditorServices/Extensions/EditorRequests.cs
Original file line number Diff line number Diff line change
@@ -25,10 +25,10 @@ internal class ExtensionCommandRemovedNotification
internal class GetEditorContextRequest
{ }

internal enum EditorCommandResponse
internal enum EditorOperationResponse
{
Unsupported,
OK
Completed,
Failed
}

internal class InsertTextRequest
3 changes: 0 additions & 3 deletions src/PowerShellEditorServices/Extensions/EditorWindow.cs
Original file line number Diff line number Diff line change
@@ -39,8 +39,6 @@ internal EditorWindow(IEditorOperations editorOperations)
#endregion

#region Public Methods
#pragma warning disable VSTHRD002 // These are public APIs that use async internal methods.

/// <summary>
/// Shows an informational message to the user.
/// </summary>
@@ -72,7 +70,6 @@ internal EditorWindow(IEditorOperations editorOperations)
/// <param name="timeout">A timeout in milliseconds for how long the message should remain visible.</param>
public void SetStatusBarMessage(string message, int timeout) => editorOperations.SetStatusBarMessageAsync(message, timeout).Wait();

#pragma warning restore VSTHRD002
#endregion
}
}
44 changes: 37 additions & 7 deletions src/PowerShellEditorServices/Extensions/EditorWorkspace.cs
Original file line number Diff line number Diff line change
@@ -18,10 +18,16 @@ public sealed class EditorWorkspace
#region Properties

/// <summary>
/// Gets the current workspace path if there is one for the open editor or null otherwise.
/// Gets the server's initial working directory, since the extension API doesn't have a
/// multi-root workspace concept.
/// </summary>
public string Path => editorOperations.GetWorkspacePath();

/// <summary>
/// Get all the workspace folders' paths.
/// </summary>
public string[] Paths => editorOperations.GetWorkspacePaths();

#endregion

#region Constructors
@@ -31,30 +37,54 @@ public sealed class EditorWorkspace
#endregion

#region Public Methods
#pragma warning disable VSTHRD002 // These are public APIs that use async internal methods.
// TODO: Consider returning bool instead of void to indicate success?

/// <summary>
/// Creates a new file in the editor.
/// </summary>
public void NewFile() => editorOperations.NewFileAsync(string.Empty).Wait();

/// <summary>
/// Creates a new file in the editor
/// Creates a new file in the editor.
/// </summary>
public void NewFile() => editorOperations.NewFileAsync().Wait();
/// <param name="content">The content to place in the new file.</param>
public void NewFile(string content) => editorOperations.NewFileAsync(content).Wait();

/// <summary>
/// Opens a file in the workspace. If the file is already open
/// Opens a file in the workspace. If the file is already open
/// its buffer will be made active.
/// </summary>
/// <param name="filePath">The path to the file to be opened.</param>
public void OpenFile(string filePath) => editorOperations.OpenFileAsync(filePath).Wait();

/// <summary>
/// Opens a file in the workspace. If the file is already open
/// Opens a file in the workspace. If the file is already open
/// its buffer will be made active.
/// You can specify whether the file opens as a preview or as a durable editor.
/// </summary>
/// <param name="filePath">The path to the file to be opened.</param>
/// <param name="preview">Determines wether the file is opened as a preview or as a durable editor.</param>
public void OpenFile(string filePath, bool preview) => editorOperations.OpenFileAsync(filePath, preview).Wait();

#pragma warning restore VSTHRD002
/// <summary>
/// Closes a file in the workspace.
/// </summary>
/// <param name="filePath">The path to the file to be closed.</param>
public void CloseFile(string filePath) => editorOperations.CloseFileAsync(filePath).Wait();

/// <summary>
/// Saves an open file in the workspace.
/// </summary>
/// <param name="filePath">The path to the file to be saved.</param>
public void SaveFile(string filePath) => editorOperations.SaveFileAsync(filePath).Wait();

/// <summary>
/// Saves a file with a new name AKA a copy.
/// </summary>
/// <param name="oldFilePath">The file to copy.</param>
/// <param name="newFilePath">The file to create.</param>
public void SaveFile(string oldFilePath, string newFilePath) => editorOperations.SaveFileAsync(oldFilePath, newFilePath).Wait();

#endregion
}
}
3 changes: 1 addition & 2 deletions src/PowerShellEditorServices/Extensions/FileContext.cs
Original file line number Diff line number Diff line change
@@ -58,8 +58,7 @@ public sealed class FileContext
/// <summary>
/// Gets the workspace-relative path of the file.
/// </summary>
public string WorkspacePath => editorOperations.GetWorkspaceRelativePath(
scriptFile.FilePath);
public string WorkspacePath => editorOperations.GetWorkspaceRelativePath(scriptFile);

#endregion

21 changes: 17 additions & 4 deletions src/PowerShellEditorServices/Extensions/IEditorOperations.cs
Original file line number Diff line number Diff line change
@@ -20,24 +20,37 @@ internal interface IEditorOperations
Task<EditorContext> GetEditorContextAsync();

/// <summary>
/// Gets the path to the editor's active workspace.
/// Gets the server's initial working directory, since the extension API doesn't have a
/// multi-root workspace concept.
/// </summary>
/// <returns>The workspace path or null if there isn't one.</returns>
/// <returns>The server's initial working directory.</returns>
string GetWorkspacePath();

/// <summary>
/// Get all the workspace folders' paths.
/// </summary>
/// <returns></returns>
string[] GetWorkspacePaths();

/// <summary>
/// Resolves the given file path relative to the current workspace path.
/// </summary>
/// <param name="filePath">The file path to be resolved.</param>
/// <returns>The resolved file path.</returns>
string GetWorkspaceRelativePath(string filePath);
string GetWorkspaceRelativePath(ScriptFile scriptFile);

/// <summary>
/// Causes a new untitled file to be created in the editor.
/// </summary>
/// <returns>A task that can be awaited for completion.</returns>
Task NewFileAsync();

/// <summary>
/// Causes a new untitled file to be created in the editor.
/// </summary>
/// <param name="content">The content to insert into the new file.</param>
/// <returns>A task that can be awaited for completion.</returns>
Task NewFileAsync(string content);

/// <summary>
/// Causes a file to be opened in the editor. If the file is
/// already open, the editor must switch to the file.
Original file line number Diff line number Diff line change
@@ -6,18 +6,16 @@
using Microsoft.PowerShell.EditorServices.Services.TextDocument;
using OmniSharp.Extensions.LanguageServer.Protocol.Models;
using OmniSharp.Extensions.LanguageServer.Protocol.Server;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace Microsoft.PowerShell.EditorServices.Services.Extension
{
internal class EditorOperationsService : IEditorOperations
{
private const bool DefaultPreviewSetting = true;

private readonly PsesInternalHost _psesHost;
private readonly WorkspaceService _workspaceService;

private readonly ILanguageServerFacade _languageServer;

public EditorOperationsService(
@@ -72,7 +70,7 @@ public async Task InsertTextAsync(string filePath, string text, BufferRange inse
Character = insertRange.End.Column - 1
}
}
}).ReturningVoid(CancellationToken.None).ConfigureAwait(false);
}).Returning<EditorOperationResponse>(CancellationToken.None).ConfigureAwait(false);
}

public async Task SetSelectionAsync(BufferRange selectionRange)
@@ -98,7 +96,7 @@ public async Task SetSelectionAsync(BufferRange selectionRange)
Character = selectionRange.End.Column - 1
}
}
}).ReturningVoid(CancellationToken.None).ConfigureAwait(false);
}).Returning<EditorOperationResponse>(CancellationToken.None).ConfigureAwait(false);
}

public EditorContext ConvertClientEditorContext(
@@ -123,15 +121,17 @@ public EditorContext ConvertClientEditorContext(
clientContext.CurrentFileLanguage);
}

public async Task NewFileAsync()
public async Task NewFileAsync() => await NewFileAsync(string.Empty).ConfigureAwait(false);

public async Task NewFileAsync(string content)
{
if (!TestHasLanguageServer())
{
return;
}

await _languageServer.SendRequest<string>("editor/newFile", null)
.ReturningVoid(CancellationToken.None)
await _languageServer.SendRequest("editor/newFile", content)
.Returning<EditorOperationResponse>(CancellationToken.None)
.ConfigureAwait(false);
}

@@ -145,8 +145,8 @@ public async Task OpenFileAsync(string filePath)
await _languageServer.SendRequest("editor/openFile", new OpenFileDetails
{
FilePath = filePath,
Preview = DefaultPreviewSetting
}).ReturningVoid(CancellationToken.None).ConfigureAwait(false);
Preview = true
}).Returning<EditorOperationResponse>(CancellationToken.None).ConfigureAwait(false);
}

public async Task OpenFileAsync(string filePath, bool preview)
@@ -160,7 +160,7 @@ public async Task OpenFileAsync(string filePath, bool preview)
{
FilePath = filePath,
Preview = preview
}).ReturningVoid(CancellationToken.None).ConfigureAwait(false);
}).Returning<EditorOperationResponse>(CancellationToken.None).ConfigureAwait(false);
}

public async Task CloseFileAsync(string filePath)
@@ -171,7 +171,7 @@ public async Task CloseFileAsync(string filePath)
}

await _languageServer.SendRequest("editor/closeFile", filePath)
.ReturningVoid(CancellationToken.None)
.Returning<EditorOperationResponse>(CancellationToken.None)
.ConfigureAwait(false);
}

@@ -188,14 +188,16 @@ public async Task SaveFileAsync(string currentPath, string newSavePath)
{
FilePath = currentPath,
NewPath = newSavePath
}).ReturningVoid(CancellationToken.None).ConfigureAwait(false);
}).Returning<EditorOperationResponse>(CancellationToken.None).ConfigureAwait(false);
}

// TODO: This should get the current editor's context and use it to determine which
// workspace it's in.
// NOTE: This name is now outdated since we don't have a way to distinguish one workspace
// from another for the extension API.
public string GetWorkspacePath() => _workspaceService.InitialWorkingDirectory;

public string GetWorkspaceRelativePath(string filePath) => _workspaceService.GetRelativePath(filePath);
public string[] GetWorkspacePaths() => _workspaceService.WorkspacePaths.ToArray();

public string GetWorkspaceRelativePath(ScriptFile scriptFile) => _workspaceService.GetRelativePath(scriptFile);

public async Task ShowInformationMessageAsync(string message)
{
@@ -205,7 +207,7 @@ public async Task ShowInformationMessageAsync(string message)
}

await _languageServer.SendRequest("editor/showInformationMessage", message)
.ReturningVoid(CancellationToken.None)
.Returning<EditorOperationResponse>(CancellationToken.None)
.ConfigureAwait(false);
}

@@ -217,7 +219,7 @@ public async Task ShowErrorMessageAsync(string message)
}

await _languageServer.SendRequest("editor/showErrorMessage", message)
.ReturningVoid(CancellationToken.None)
.Returning<EditorOperationResponse>(CancellationToken.None)
.ConfigureAwait(false);
}

@@ -229,7 +231,7 @@ public async Task ShowWarningMessageAsync(string message)
}

await _languageServer.SendRequest("editor/showWarningMessage", message)
.ReturningVoid(CancellationToken.None)
.Returning<EditorOperationResponse>(CancellationToken.None)
.ConfigureAwait(false);
}

@@ -244,7 +246,7 @@ public async Task SetStatusBarMessageAsync(string message, int? timeout)
{
Message = message,
Timeout = timeout
}).ReturningVoid(CancellationToken.None).ConfigureAwait(false);
}).Returning<EditorOperationResponse>(CancellationToken.None).ConfigureAwait(false);
}

public void ClearTerminal()
@@ -267,7 +269,7 @@ private bool TestHasLanguageServer(bool warnUser = true)
if (warnUser)
{
_psesHost.UI.WriteWarningLine(
"Editor operations are not supported in temporary consoles. Re-run the command in the main PowerShell Intergrated Console.");
"Editor operations are not supported in temporary consoles. Re-run the command in the main Extension Terminal.");
}

return false;
Original file line number Diff line number Diff line change
@@ -51,7 +51,7 @@ public async Task<CommentHelpRequestResult> Handle(CommentHelpRequestParams requ
{
// check if the previous character is `<` because it invalidates
// the param block the follows it.
IList<string> lines = ScriptFile.GetLinesInternal(funcText);
IList<string> lines = ScriptFile.GetLines(funcText);
int relativeTriggerLine0b = triggerLine - funcExtent.StartLineNumber;
if (relativeTriggerLine0b > 0 && lines[relativeTriggerLine0b].IndexOf("<", StringComparison.OrdinalIgnoreCase) > -1)
{
@@ -68,7 +68,7 @@ public async Task<CommentHelpRequestResult> Handle(CommentHelpRequestParams requ
return result;
}

List<string> helpLines = ScriptFile.GetLinesInternal(helpText);
List<string> helpLines = ScriptFile.GetLines(helpText);

if (helpLocation?.Equals("before", StringComparison.OrdinalIgnoreCase) == false)
{
18 changes: 2 additions & 16 deletions src/PowerShellEditorServices/Services/TextDocument/ScriptFile.cs
Original file line number Diff line number Diff line change
@@ -31,13 +31,6 @@ internal sealed 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 => FilePath.ToLower();

/// <summary>
/// Gets the path at which this file resides.
/// </summary>
@@ -173,14 +166,7 @@ internal ScriptFile(
/// </summary>
/// <param name="text">Input string to be split up into lines.</param>
/// <returns>The lines in the string.</returns>
internal static IList<string> GetLines(string text) => GetLinesInternal(text);

/// <summary>
/// Get the lines in a string.
/// </summary>
/// <param name="text">Input string to be split up into lines.</param>
/// <returns>The lines in the string.</returns>
internal static List<string> GetLinesInternal(string text)
internal static List<string> GetLines(string text)
{
if (text == null)
{
@@ -520,7 +506,7 @@ internal void SetFileContents(string fileContents)
{
// Split the file contents into lines and trim
// any carriage returns from the strings.
FileLines = GetLinesInternal(fileContents);
FileLines = GetLines(fileContents);

// Parse the contents to get syntax tree and errors
ParseFileContents();
78 changes: 23 additions & 55 deletions src/PowerShellEditorServices/Services/Workspace/WorkspaceService.cs
Original file line number Diff line number Diff line change
@@ -104,6 +104,10 @@ public WorkspaceService(ILoggerFactory factory)

#region Public Methods

public IEnumerable<string> WorkspacePaths => WorkspaceFolders.Count == 0
? new List<string> { InitialWorkingDirectory }
: WorkspaceFolders.Select(i => i.Uri.GetFileSystemPath());

/// <summary>
/// Gets an open file in the workspace. If the file isn't open but exists on the filesystem, load and return it.
/// <para>IMPORTANT: Not all documents have a backing file e.g. untitled: scheme documents. Consider using
@@ -306,27 +310,31 @@ public void CloseFile(ScriptFile scriptFile)
/// <summary>
/// Gets the workspace-relative path of the given file path.
/// </summary>
/// <param name="filePath">The original full file path.</param>
/// <returns>A relative file path</returns>
public string GetRelativePath(string filePath)
public string GetRelativePath(ScriptFile scriptFile)
{
string resolvedPath = filePath;

if (!IsPathInMemory(filePath) && !string.IsNullOrEmpty(InitialWorkingDirectory))
Uri fileUri = scriptFile.DocumentUri.ToUri();
if (!scriptFile.IsInMemory)
{
Uri workspaceUri = new(InitialWorkingDirectory);
Uri fileUri = new(filePath);

resolvedPath = workspaceUri.MakeRelativeUri(fileUri).ToString();

// Convert the directory separators if necessary
if (Path.DirectorySeparatorChar == '\\')
// Support calculating out-of-workspace relative paths in the common case of a
// single workspace folder. Otherwise try to get the matching folder.
foreach (WorkspaceFolder workspaceFolder in WorkspaceFolders)
{
resolvedPath = resolvedPath.Replace('/', '\\');
Uri workspaceUri = workspaceFolder.Uri.ToUri();
if (WorkspaceFolders.Count == 1 || workspaceUri.IsBaseOf(fileUri))
{
return workspaceUri.MakeRelativeUri(fileUri).ToString();
}
}
}

return resolvedPath;
// Default to the absolute file path if possible, otherwise just return the URI. This
// removes the scheme and initial slash when possible.
if (fileUri.IsAbsoluteUri)
{
return fileUri.AbsolutePath;
}
return fileUri.ToString();
}

/// <summary>
@@ -354,15 +362,11 @@ public IEnumerable<string> EnumeratePSFiles(
int maxDepth,
bool ignoreReparsePoints)
{
IEnumerable<string> rootPaths = WorkspaceFolders.Count == 0
? new List<string> { InitialWorkingDirectory }
: WorkspaceFolders.Select(i => i.Uri.GetFileSystemPath());

Matcher matcher = new();
foreach (string pattern in includeGlobs) { matcher.AddInclude(pattern); }
foreach (string pattern in excludeGlobs) { matcher.AddExclude(pattern); }

foreach (string rootPath in rootPaths)
foreach (string rootPath in WorkspacePaths)
{
if (!Directory.Exists(rootPath))
{
@@ -407,42 +411,6 @@ internal static string ReadFileContents(DocumentUri uri)
return reader.ReadToEnd();
}

internal static bool IsPathInMemory(string filePath)
{
bool isInMemory = false;

// In cases where a "virtual" file is displayed in the editor,
// we need to treat the file differently than one that exists
// on disk. A virtual file could be something like a diff
// view of the current file or an untitled file.
try
{
// File system absolute paths will have a URI scheme of file:.
// Other schemes like "untitled:" and "gitlens-git:" will return false for IsFile.
Uri uri = new(filePath);
isInMemory = !uri.IsFile;
}
catch (UriFormatException)
{
// Relative file paths cause a UriFormatException.
// In this case, fallback to using Path.GetFullPath().
try
{
Path.GetFullPath(filePath);
}
catch (Exception ex) when (ex is ArgumentException or NotSupportedException)
{
isInMemory = true;
}
catch (PathTooLongException)
{
// If we ever get here, it should be an actual file so, not in memory
}
}

return isInMemory;
}

internal string ResolveWorkspacePath(string path) => ResolveRelativeScriptPath(InitialWorkingDirectory, path);

internal string ResolveRelativeScriptPath(string baseFilePath, string relativePath)
91 changes: 46 additions & 45 deletions test/PowerShellEditorServices.Test/Session/WorkspaceTests.cs
Original file line number Diff line number Diff line change
@@ -10,6 +10,9 @@
using Microsoft.PowerShell.EditorServices.Test.Shared;
using Microsoft.PowerShell.EditorServices.Services.TextDocument;
using Xunit;
using Microsoft.PowerShell.EditorServices.Utility;
using OmniSharp.Extensions.LanguageServer.Protocol.Models;
using OmniSharp.Extensions.LanguageServer.Protocol;

namespace PowerShellEditorServices.Test.Session
{
@@ -22,27 +25,51 @@ public class WorkspaceTests
? s_lazyDriveLetter.Value
: string.Empty;

internal static ScriptFile CreateScriptFile(string path) => new(path, "", VersionUtils.PSVersion);


[Fact]
public void CanResolveWorkspaceRelativePath()
{
string workspacePath = TestUtilities.NormalizePath("c:/Test/Workspace/");
string testPathInside = TestUtilities.NormalizePath("c:/Test/Workspace/SubFolder/FilePath.ps1");
string testPathOutside = TestUtilities.NormalizePath("c:/Test/PeerPath/FilePath.ps1");
string testPathAnotherDrive = TestUtilities.NormalizePath("z:/TryAndFindMe/FilePath.ps1");
string workspacePath = "c:/Test/Workspace/";
ScriptFile testPathInside = CreateScriptFile("c:/Test/Workspace/SubFolder/FilePath.ps1");
ScriptFile testPathOutside = CreateScriptFile("c:/Test/PeerPath/FilePath.ps1");
ScriptFile testPathAnotherDrive = CreateScriptFile("z:/TryAndFindMe/FilePath.ps1");

WorkspaceService workspace = new(NullLoggerFactory.Instance);

// Test without a workspace path
Assert.Equal(testPathOutside, workspace.GetRelativePath(testPathOutside));
// Test with zero workspace folders
Assert.Equal(
testPathOutside.DocumentUri.ToUri().AbsolutePath,
workspace.GetRelativePath(testPathOutside));

string expectedInsidePath = "SubFolder/FilePath.ps1";
string expectedOutsidePath = "../PeerPath/FilePath.ps1";

string expectedInsidePath = TestUtilities.NormalizePath("SubFolder/FilePath.ps1");
string expectedOutsidePath = TestUtilities.NormalizePath("../PeerPath/FilePath.ps1");
// Test with a single workspace folder
workspace.WorkspaceFolders.Add(new WorkspaceFolder
{
Uri = DocumentUri.FromFileSystemPath(workspacePath)
});

// Test with a workspace path
workspace.InitialWorkingDirectory = workspacePath;
Assert.Equal(expectedInsidePath, workspace.GetRelativePath(testPathInside));
Assert.Equal(expectedOutsidePath, workspace.GetRelativePath(testPathOutside));
Assert.Equal(testPathAnotherDrive, workspace.GetRelativePath(testPathAnotherDrive));
Assert.Equal(
testPathAnotherDrive.DocumentUri.ToUri().AbsolutePath,
workspace.GetRelativePath(testPathAnotherDrive));

// Test with two workspace folders
string anotherWorkspacePath = "c:/Test/AnotherWorkspace/";
ScriptFile anotherTestPathInside = CreateScriptFile("c:/Test/AnotherWorkspace/DifferentFolder/FilePath.ps1");
string anotherExpectedInsidePath = "DifferentFolder/FilePath.ps1";

workspace.WorkspaceFolders.Add(new WorkspaceFolder
{
Uri = DocumentUri.FromFileSystemPath(anotherWorkspacePath)
});

Assert.Equal(expectedInsidePath, workspace.GetRelativePath(testPathInside));
Assert.Equal(anotherExpectedInsidePath, workspace.GetRelativePath(anotherTestPathInside));
}

internal static WorkspaceService FixturesWorkspace()
@@ -53,6 +80,14 @@ internal static WorkspaceService FixturesWorkspace()
};
}

[Fact]
public void HasDefaultForWorkspacePaths()
{
WorkspaceService workspace = FixturesWorkspace();
string actual = Assert.Single(workspace.WorkspacePaths);
Assert.Equal(workspace.InitialWorkingDirectory, actual);
}

// These are the default values for the EnumeratePSFiles() method
// in Microsoft.PowerShell.EditorServices.Workspace class
private static readonly string[] s_defaultExcludeGlobs = Array.Empty<string>();
@@ -143,40 +178,6 @@ public void CanRecurseDirectoryTreeWithGlobs()
}, actual);
}

[Fact]
public void CanDetermineIsPathInMemory()
{
string tempDir = Path.GetTempPath();
string shortDirPath = Path.Combine(tempDir, "GitHub", "PowerShellEditorServices");
string shortFilePath = Path.Combine(shortDirPath, "foo.ps1");
const string shortUriForm = "git:/c%3A/Users/Keith/GitHub/dahlbyk/posh-git/src/PoshGitTypes.ps1?%7B%22path%22%3A%22c%3A%5C%5CUsers%5C%5CKeith%5C%5CGitHub%5C%5Cdahlbyk%5C%5Cposh-git%5C%5Csrc%5C%5CPoshGitTypes.ps1%22%2C%22ref%22%3A%22~%22%7D";
const string longUriForm = "gitlens-git:c%3A%5CUsers%5CKeith%5CGitHub%5Cdahlbyk%5Cposh-git%5Csrc%5CPoshGitTypes%3Ae0022701.ps1?%7B%22fileName%22%3A%22src%2FPoshGitTypes.ps1%22%2C%22repoPath%22%3A%22c%3A%2FUsers%2FKeith%2FGitHub%2Fdahlbyk%2Fposh-git%22%2C%22sha%22%3A%22e0022701fa12e0bc22d0458673d6443c942b974a%22%7D";

string[] inMemoryPaths = new[] {
// Test short non-file paths
"untitled:untitled-1",
shortUriForm,
"inmemory://foo.ps1",
// Test long non-file path
longUriForm
};

Assert.All(inMemoryPaths, (p) => Assert.True(WorkspaceService.IsPathInMemory(p)));

string[] notInMemoryPaths = new[] {
// Test short file absolute paths
shortDirPath,
shortFilePath,
new Uri(shortDirPath).ToString(),
new Uri(shortFilePath).ToString(),
// Test short file relative paths
"foo.ps1",
Path.Combine(new[] { "..", "foo.ps1" })
};

Assert.All(notInMemoryPaths, (p) => Assert.False(WorkspaceService.IsPathInMemory(p)));
}

[Fact]
public void CanOpenAndCloseFile()
{