From 06342e3055aaec33f367f4f2c1fbc8be81fc06c8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 12 May 2026 17:28:09 +0000 Subject: [PATCH 01/31] Initial plan From 4db3d1dd074b24f6da2771746b8367d2276b6fd3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 12 May 2026 17:32:44 +0000 Subject: [PATCH 02/31] Expose open workspace documents on PSEditor workspace API Agent-Logs-Url: https://github.com/PowerShell/PowerShellEditorServices/sessions/76ae345c-b025-4fa0-8a0c-f6f8fa243d80 Co-authored-by: JustinGrote <15258962+JustinGrote@users.noreply.github.com> --- .../Extensions/EditorWorkspace.cs | 47 ++++++++ .../Extensions/IEditorOperations.cs | 6 + .../Extension/EditorOperationsService.cs | 2 + .../Extensions/EditorWorkspaceTests.cs | 110 ++++++++++++++++++ 4 files changed, 165 insertions(+) create mode 100644 test/PowerShellEditorServices.Test/Extensions/EditorWorkspaceTests.cs diff --git a/src/PowerShellEditorServices/Extensions/EditorWorkspace.cs b/src/PowerShellEditorServices/Extensions/EditorWorkspace.cs index e1e0824cb..47fcca378 100644 --- a/src/PowerShellEditorServices/Extensions/EditorWorkspace.cs +++ b/src/PowerShellEditorServices/Extensions/EditorWorkspace.cs @@ -3,6 +3,35 @@ namespace Microsoft.PowerShell.EditorServices.Extensions { + /// + /// A document currently open in the editor workspace. + /// + public sealed class EditorWorkspaceDocument + { + private readonly EditorWorkspace _workspace; + + internal EditorWorkspaceDocument(EditorWorkspace workspace, string path) + { + _workspace = workspace; + Path = path; + } + + /// + /// Gets the path of the document. + /// + public string Path { get; } + + /// + /// Opens this document in the editor. + /// + public void Open() => _workspace.OpenFile(Path); + + /// + /// Saves this document in the editor. + /// + public void Save() => _workspace.SaveFile(Path); + } + /// /// Provides a PowerShell-facing API which allows scripts to /// interact with the editor's workspace. @@ -28,6 +57,24 @@ public sealed class EditorWorkspace /// public string[] Paths => editorOperations.GetWorkspacePaths(); + /// + /// Get all currently open documents in the workspace. + /// + public EditorWorkspaceDocument[] Documents + { + get + { + string[] openDocumentPaths = editorOperations.GetWorkspaceOpenDocumentPaths(); + EditorWorkspaceDocument[] documents = new EditorWorkspaceDocument[openDocumentPaths.Length]; + for (int i = 0; i < openDocumentPaths.Length; i++) + { + documents[i] = new EditorWorkspaceDocument(this, openDocumentPaths[i]); + } + + return documents; + } + } + #endregion #region Constructors diff --git a/src/PowerShellEditorServices/Extensions/IEditorOperations.cs b/src/PowerShellEditorServices/Extensions/IEditorOperations.cs index 3ec33ebc6..909f11255 100644 --- a/src/PowerShellEditorServices/Extensions/IEditorOperations.cs +++ b/src/PowerShellEditorServices/Extensions/IEditorOperations.cs @@ -32,6 +32,12 @@ internal interface IEditorOperations /// string[] GetWorkspacePaths(); + /// + /// Get all open document paths in the current workspace session. + /// + /// All currently open document paths. + string[] GetWorkspaceOpenDocumentPaths(); + /// /// Resolves the given file path relative to the current workspace path. /// diff --git a/src/PowerShellEditorServices/Services/Extension/EditorOperationsService.cs b/src/PowerShellEditorServices/Services/Extension/EditorOperationsService.cs index 607d38720..aef0ad033 100644 --- a/src/PowerShellEditorServices/Services/Extension/EditorOperationsService.cs +++ b/src/PowerShellEditorServices/Services/Extension/EditorOperationsService.cs @@ -198,6 +198,8 @@ public async Task SaveFileAsync(string currentPath, string newSavePath) public string[] GetWorkspacePaths() => _workspaceService.WorkspacePaths.ToArray(); + public string[] GetWorkspaceOpenDocumentPaths() => _workspaceService.GetOpenedFiles().Select(static scriptFile => scriptFile.FilePath).ToArray(); + public string GetWorkspaceRelativePath(ScriptFile scriptFile) => _workspaceService.GetRelativePath(scriptFile); public async Task ShowInformationMessageAsync(string message) diff --git a/test/PowerShellEditorServices.Test/Extensions/EditorWorkspaceTests.cs b/test/PowerShellEditorServices.Test/Extensions/EditorWorkspaceTests.cs new file mode 100644 index 000000000..a499040f7 --- /dev/null +++ b/test/PowerShellEditorServices.Test/Extensions/EditorWorkspaceTests.cs @@ -0,0 +1,110 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.PowerShell.EditorServices.Extensions; +using Microsoft.PowerShell.EditorServices.Services.TextDocument; +using Xunit; + +namespace PowerShellEditorServices.Test.Extensions +{ + [Trait("Category", "Extensions")] + public class EditorWorkspaceTests + { + [Fact] + public void DocumentsReturnsOpenWorkspaceDocuments() + { + TestEditorOperations editorOperations = new() + { + OpenDocumentPaths = new[] { @"C:\test\one.ps1", @"C:\test\two.ps1" } + }; + + EditorWorkspace workspace = new(editorOperations); + + EditorWorkspaceDocument[] documents = workspace.Documents; + + Assert.Collection( + documents, + document => Assert.Equal(@"C:\test\one.ps1", document.Path), + document => Assert.Equal(@"C:\test\two.ps1", document.Path)); + } + + [Fact] + public void DocumentOpenAndSaveUseWorkspaceOperations() + { + const string filePath = @"C:\test\file.ps1"; + TestEditorOperations editorOperations = new() + { + OpenDocumentPaths = new[] { filePath } + }; + + EditorWorkspace workspace = new(editorOperations); + EditorWorkspaceDocument document = Assert.Single(workspace.Documents); + + document.Open(); + document.Save(); + + Assert.Collection( + editorOperations.Calls, + call => Assert.Equal("OpenFile:" + filePath, call), + call => Assert.Equal("SaveFile:" + filePath, call)); + } + + private sealed class TestEditorOperations : IEditorOperations + { + public string[] OpenDocumentPaths { get; set; } = Array.Empty(); + + public List Calls { get; } = new(); + + public Task GetEditorContextAsync() => Task.FromResult(default(EditorContext)); + + public string GetWorkspacePath() => @"C:\test"; + + public string[] GetWorkspacePaths() => new[] { @"C:\test" }; + + public string[] GetWorkspaceOpenDocumentPaths() => OpenDocumentPaths; + + public string GetWorkspaceRelativePath(ScriptFile scriptFile) => scriptFile.FilePath; + + public Task NewFileAsync() => Task.CompletedTask; + + public Task NewFileAsync(string content) => Task.CompletedTask; + + public Task OpenFileAsync(string filePath) + { + Calls.Add("OpenFile:" + filePath); + return Task.CompletedTask; + } + + public Task OpenFileAsync(string filePath, bool preview) => Task.CompletedTask; + + public Task CloseFileAsync(string filePath) => Task.CompletedTask; + + public Task SaveFileAsync(string filePath) + { + Calls.Add("SaveFile:" + filePath); + return Task.CompletedTask; + } + + public Task SaveFileAsync(string oldFilePath, string newFilePath) => Task.CompletedTask; + + public Task InsertTextAsync(string filePath, string insertText, BufferRange insertRange) => Task.CompletedTask; + + public Task SetSelectionAsync(BufferRange selectionRange) => Task.CompletedTask; + + public Task ShowInformationMessageAsync(string message) => Task.CompletedTask; + + public Task ShowErrorMessageAsync(string message) => Task.CompletedTask; + + public Task ShowWarningMessageAsync(string message) => Task.CompletedTask; + + public Task SetStatusBarMessageAsync(string message, int? timeout) => Task.CompletedTask; + + public void ClearTerminal() + { + } + } + } +} From ac478395a8a148effc493f04d78f8ba495345ba1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 12 May 2026 17:52:50 +0000 Subject: [PATCH 03/31] Add close operation to workspace documents API Agent-Logs-Url: https://github.com/PowerShell/PowerShellEditorServices/sessions/6a4ffcb3-ffa5-431d-8a79-d5c87df48a9c Co-authored-by: JustinGrote <15258962+JustinGrote@users.noreply.github.com> --- .../Extensions/EditorWorkspace.cs | 5 +++++ .../Extensions/EditorWorkspaceTests.cs | 12 +++++++++--- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/PowerShellEditorServices/Extensions/EditorWorkspace.cs b/src/PowerShellEditorServices/Extensions/EditorWorkspace.cs index 47fcca378..0a6e764fc 100644 --- a/src/PowerShellEditorServices/Extensions/EditorWorkspace.cs +++ b/src/PowerShellEditorServices/Extensions/EditorWorkspace.cs @@ -30,6 +30,11 @@ internal EditorWorkspaceDocument(EditorWorkspace workspace, string path) /// Saves this document in the editor. /// public void Save() => _workspace.SaveFile(Path); + + /// + /// Closes this document in the editor. + /// + public void Close() => _workspace.CloseFile(Path); } /// diff --git a/test/PowerShellEditorServices.Test/Extensions/EditorWorkspaceTests.cs b/test/PowerShellEditorServices.Test/Extensions/EditorWorkspaceTests.cs index a499040f7..c2a265347 100644 --- a/test/PowerShellEditorServices.Test/Extensions/EditorWorkspaceTests.cs +++ b/test/PowerShellEditorServices.Test/Extensions/EditorWorkspaceTests.cs @@ -32,7 +32,7 @@ public void DocumentsReturnsOpenWorkspaceDocuments() } [Fact] - public void DocumentOpenAndSaveUseWorkspaceOperations() + public void DocumentOpenSaveAndCloseUseWorkspaceOperations() { const string filePath = @"C:\test\file.ps1"; TestEditorOperations editorOperations = new() @@ -45,11 +45,13 @@ public void DocumentOpenAndSaveUseWorkspaceOperations() document.Open(); document.Save(); + document.Close(); Assert.Collection( editorOperations.Calls, call => Assert.Equal("OpenFile:" + filePath, call), - call => Assert.Equal("SaveFile:" + filePath, call)); + call => Assert.Equal("SaveFile:" + filePath, call), + call => Assert.Equal("CloseFile:" + filePath, call)); } private sealed class TestEditorOperations : IEditorOperations @@ -80,7 +82,11 @@ public Task OpenFileAsync(string filePath) public Task OpenFileAsync(string filePath, bool preview) => Task.CompletedTask; - public Task CloseFileAsync(string filePath) => Task.CompletedTask; + public Task CloseFileAsync(string filePath) + { + Calls.Add("CloseFile:" + filePath); + return Task.CompletedTask; + } public Task SaveFileAsync(string filePath) { From c8721bab527148f9301cd2b2e554de6ed3866db1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 12 May 2026 18:14:07 +0000 Subject: [PATCH 04/31] Add ToString override for workspace documents Agent-Logs-Url: https://github.com/PowerShell/PowerShellEditorServices/sessions/7e3e8a1d-f66b-4f87-8bde-eb0dd19f928c Co-authored-by: JustinGrote <15258962+JustinGrote@users.noreply.github.com> --- .../Extensions/EditorWorkspace.cs | 6 ++++++ .../Extensions/EditorWorkspaceTests.cs | 15 +++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/src/PowerShellEditorServices/Extensions/EditorWorkspace.cs b/src/PowerShellEditorServices/Extensions/EditorWorkspace.cs index 0a6e764fc..b6b0abe0c 100644 --- a/src/PowerShellEditorServices/Extensions/EditorWorkspace.cs +++ b/src/PowerShellEditorServices/Extensions/EditorWorkspace.cs @@ -21,6 +21,12 @@ internal EditorWorkspaceDocument(EditorWorkspace workspace, string path) /// public string Path { get; } + /// + /// Gets the full path of this document. + /// + /// The full document path. + public override string ToString() => Path; + /// /// Opens this document in the editor. /// diff --git a/test/PowerShellEditorServices.Test/Extensions/EditorWorkspaceTests.cs b/test/PowerShellEditorServices.Test/Extensions/EditorWorkspaceTests.cs index c2a265347..7414dce27 100644 --- a/test/PowerShellEditorServices.Test/Extensions/EditorWorkspaceTests.cs +++ b/test/PowerShellEditorServices.Test/Extensions/EditorWorkspaceTests.cs @@ -54,6 +54,21 @@ public void DocumentOpenSaveAndCloseUseWorkspaceOperations() call => Assert.Equal("CloseFile:" + filePath, call)); } + [Fact] + public void DocumentToStringReturnsDocumentPath() + { + const string filePath = @"C:\test\file.ps1"; + TestEditorOperations editorOperations = new() + { + OpenDocumentPaths = new[] { filePath } + }; + + EditorWorkspace workspace = new(editorOperations); + EditorWorkspaceDocument document = Assert.Single(workspace.Documents); + + Assert.Equal(filePath, document.ToString()); + } + private sealed class TestEditorOperations : IEditorOperations { public string[] OpenDocumentPaths { get; set; } = Array.Empty(); From 9b0f3728eb6986351634394d167a8a5b57f790ff Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 12 May 2026 18:35:36 +0000 Subject: [PATCH 05/31] Add Saved status to workspace documents Agent-Logs-Url: https://github.com/PowerShell/PowerShellEditorServices/sessions/3e87bc0c-ffcc-4ce4-9b08-22d248a3f5db Co-authored-by: JustinGrote <15258962+JustinGrote@users.noreply.github.com> --- .../Extensions/EditorWorkspace.cs | 26 +++++--- .../Extensions/IEditorOperations.cs | 19 +++++- .../Extension/EditorOperationsService.cs | 5 +- .../Handlers/TextDocumentHandler.cs | 2 + .../Services/TextDocument/ScriptFile.cs | 7 +++ .../Extensions/EditorWorkspaceTests.cs | 63 +++++++++++++++---- 6 files changed, 99 insertions(+), 23 deletions(-) diff --git a/src/PowerShellEditorServices/Extensions/EditorWorkspace.cs b/src/PowerShellEditorServices/Extensions/EditorWorkspace.cs index b6b0abe0c..19c5dac6a 100644 --- a/src/PowerShellEditorServices/Extensions/EditorWorkspace.cs +++ b/src/PowerShellEditorServices/Extensions/EditorWorkspace.cs @@ -10,10 +10,11 @@ public sealed class EditorWorkspaceDocument { private readonly EditorWorkspace _workspace; - internal EditorWorkspaceDocument(EditorWorkspace workspace, string path) + internal EditorWorkspaceDocument(EditorWorkspace workspace, string path, bool saved) { _workspace = workspace; Path = path; + Saved = saved; } /// @@ -22,10 +23,19 @@ internal EditorWorkspaceDocument(EditorWorkspace workspace, string path) public string Path { get; } /// - /// Gets the full path of this document. + /// Gets whether the document has unsaved changes. /// - /// The full document path. - public override string ToString() => Path; + public bool Saved { get; } + + /// + /// Gets the display name of this document and unsaved status. + /// + /// The display name of this document. + public override string ToString() + { + string fileName = System.IO.Path.GetFileName(Path); + return Saved ? fileName : fileName + " [Unsaved]"; + } /// /// Opens this document in the editor. @@ -75,11 +85,11 @@ public EditorWorkspaceDocument[] Documents { get { - string[] openDocumentPaths = editorOperations.GetWorkspaceOpenDocumentPaths(); - EditorWorkspaceDocument[] documents = new EditorWorkspaceDocument[openDocumentPaths.Length]; - for (int i = 0; i < openDocumentPaths.Length; i++) + WorkspaceOpenDocument[] openDocuments = editorOperations.GetWorkspaceOpenDocuments(); + EditorWorkspaceDocument[] documents = new EditorWorkspaceDocument[openDocuments.Length]; + for (int i = 0; i < openDocuments.Length; i++) { - documents[i] = new EditorWorkspaceDocument(this, openDocumentPaths[i]); + documents[i] = new EditorWorkspaceDocument(this, openDocuments[i].Path, openDocuments[i].Saved); } return documents; diff --git a/src/PowerShellEditorServices/Extensions/IEditorOperations.cs b/src/PowerShellEditorServices/Extensions/IEditorOperations.cs index 909f11255..b3e629a0c 100644 --- a/src/PowerShellEditorServices/Extensions/IEditorOperations.cs +++ b/src/PowerShellEditorServices/Extensions/IEditorOperations.cs @@ -6,6 +6,19 @@ namespace Microsoft.PowerShell.EditorServices.Extensions { + internal readonly struct WorkspaceOpenDocument + { + internal WorkspaceOpenDocument(string path, bool saved) + { + Path = path; + Saved = saved; + } + + public string Path { get; } + + public bool Saved { get; } + } + /// /// Provides an interface that must be implemented by an editor /// host to perform operations invoked by extensions written in @@ -33,10 +46,10 @@ internal interface IEditorOperations string[] GetWorkspacePaths(); /// - /// Get all open document paths in the current workspace session. + /// Get all open documents in the current workspace session. /// - /// All currently open document paths. - string[] GetWorkspaceOpenDocumentPaths(); + /// All currently open documents. + WorkspaceOpenDocument[] GetWorkspaceOpenDocuments(); /// /// Resolves the given file path relative to the current workspace path. diff --git a/src/PowerShellEditorServices/Services/Extension/EditorOperationsService.cs b/src/PowerShellEditorServices/Services/Extension/EditorOperationsService.cs index aef0ad033..5260f3d39 100644 --- a/src/PowerShellEditorServices/Services/Extension/EditorOperationsService.cs +++ b/src/PowerShellEditorServices/Services/Extension/EditorOperationsService.cs @@ -198,7 +198,10 @@ public async Task SaveFileAsync(string currentPath, string newSavePath) public string[] GetWorkspacePaths() => _workspaceService.WorkspacePaths.ToArray(); - public string[] GetWorkspaceOpenDocumentPaths() => _workspaceService.GetOpenedFiles().Select(static scriptFile => scriptFile.FilePath).ToArray(); + public WorkspaceOpenDocument[] GetWorkspaceOpenDocuments() + => _workspaceService.GetOpenedFiles() + .Select(static scriptFile => new WorkspaceOpenDocument(scriptFile.FilePath, scriptFile.IsSaved)) + .ToArray(); public string GetWorkspaceRelativePath(ScriptFile scriptFile) => _workspaceService.GetRelativePath(scriptFile); diff --git a/src/PowerShellEditorServices/Services/TextDocument/Handlers/TextDocumentHandler.cs b/src/PowerShellEditorServices/Services/TextDocument/Handlers/TextDocumentHandler.cs index a02e7b884..c48c73889 100644 --- a/src/PowerShellEditorServices/Services/TextDocument/Handlers/TextDocumentHandler.cs +++ b/src/PowerShellEditorServices/Services/TextDocument/Handlers/TextDocumentHandler.cs @@ -136,6 +136,8 @@ public override async Task Handle(DidSaveTextDocumentParams notification, { await _remoteFileManagerService.SaveRemoteFileAsync(savedFile.FilePath).ConfigureAwait(false); } + + savedFile.IsSaved = true; } return Unit.Value; } diff --git a/src/PowerShellEditorServices/Services/TextDocument/ScriptFile.cs b/src/PowerShellEditorServices/Services/TextDocument/ScriptFile.cs index fb6772d2b..f4986c1ed 100644 --- a/src/PowerShellEditorServices/Services/TextDocument/ScriptFile.cs +++ b/src/PowerShellEditorServices/Services/TextDocument/ScriptFile.cs @@ -55,6 +55,11 @@ internal sealed class ScriptFile /// public bool IsInMemory { get; } + /// + /// Gets or sets whether this file has no unsaved changes. + /// + public bool IsSaved { get; internal set; } + /// /// Gets a string containing the full contents of the file. /// @@ -137,6 +142,7 @@ internal ScriptFile( // SetFileContents() calls ParseFileContents() which initializes the rest of the properties. SetFileContents(textReader.ReadToEnd()); + IsSaved = !IsInMemory; References = new ReferenceTable(this); } @@ -364,6 +370,7 @@ public void ApplyChange(FileChange fileChange) // Parse the script again to be up-to-date ParseFileContents(); + IsSaved = false; References.TagAsChanged(); } diff --git a/test/PowerShellEditorServices.Test/Extensions/EditorWorkspaceTests.cs b/test/PowerShellEditorServices.Test/Extensions/EditorWorkspaceTests.cs index 7414dce27..1fec290f1 100644 --- a/test/PowerShellEditorServices.Test/Extensions/EditorWorkspaceTests.cs +++ b/test/PowerShellEditorServices.Test/Extensions/EditorWorkspaceTests.cs @@ -18,7 +18,11 @@ public void DocumentsReturnsOpenWorkspaceDocuments() { TestEditorOperations editorOperations = new() { - OpenDocumentPaths = new[] { @"C:\test\one.ps1", @"C:\test\two.ps1" } + OpenDocuments = new[] + { + new WorkspaceOpenDocument(@"C:\test\one.ps1", saved: true), + new WorkspaceOpenDocument(@"C:\test\two.ps1", saved: true) + } }; EditorWorkspace workspace = new(editorOperations); @@ -27,8 +31,16 @@ public void DocumentsReturnsOpenWorkspaceDocuments() Assert.Collection( documents, - document => Assert.Equal(@"C:\test\one.ps1", document.Path), - document => Assert.Equal(@"C:\test\two.ps1", document.Path)); + document => + { + Assert.Equal(@"C:\test\one.ps1", document.Path); + Assert.True(document.Saved); + }, + document => + { + Assert.Equal(@"C:\test\two.ps1", document.Path); + Assert.True(document.Saved); + }); } [Fact] @@ -37,7 +49,7 @@ public void DocumentOpenSaveAndCloseUseWorkspaceOperations() const string filePath = @"C:\test\file.ps1"; TestEditorOperations editorOperations = new() { - OpenDocumentPaths = new[] { filePath } + OpenDocuments = new[] { new WorkspaceOpenDocument(filePath, saved: true) } }; EditorWorkspace workspace = new(editorOperations); @@ -55,23 +67,52 @@ public void DocumentOpenSaveAndCloseUseWorkspaceOperations() } [Fact] - public void DocumentToStringReturnsDocumentPath() + public void DocumentToStringReturnsFileNameAndSavedStatus() { - const string filePath = @"C:\test\file.ps1"; + const string savedFilePath = @"C:\test\file.ps1"; + const string unsavedFilePath = @"C:\test\other.ps1"; TestEditorOperations editorOperations = new() { - OpenDocumentPaths = new[] { filePath } + OpenDocuments = new[] + { + new WorkspaceOpenDocument(savedFilePath, saved: true), + new WorkspaceOpenDocument(unsavedFilePath, saved: false) + } }; EditorWorkspace workspace = new(editorOperations); - EditorWorkspaceDocument document = Assert.Single(workspace.Documents); + EditorWorkspaceDocument[] documents = workspace.Documents; + + Assert.Collection( + documents, + document => Assert.Equal("file.ps1", document.ToString()), + document => Assert.Equal("other.ps1 [Unsaved]", document.ToString())); + } + + [Fact] + public void DocumentSavedReturnsWorkspaceSavedState() + { + TestEditorOperations editorOperations = new() + { + OpenDocuments = new[] + { + new WorkspaceOpenDocument(@"C:\test\saved.ps1", saved: true), + new WorkspaceOpenDocument(@"C:\test\unsaved.ps1", saved: false) + } + }; - Assert.Equal(filePath, document.ToString()); + EditorWorkspace workspace = new(editorOperations); + EditorWorkspaceDocument[] documents = workspace.Documents; + + Assert.Collection( + documents, + document => Assert.True(document.Saved), + document => Assert.False(document.Saved)); } private sealed class TestEditorOperations : IEditorOperations { - public string[] OpenDocumentPaths { get; set; } = Array.Empty(); + public WorkspaceOpenDocument[] OpenDocuments { get; set; } = Array.Empty(); public List Calls { get; } = new(); @@ -81,7 +122,7 @@ private sealed class TestEditorOperations : IEditorOperations public string[] GetWorkspacePaths() => new[] { @"C:\test" }; - public string[] GetWorkspaceOpenDocumentPaths() => OpenDocumentPaths; + public WorkspaceOpenDocument[] GetWorkspaceOpenDocuments() => OpenDocuments; public string GetWorkspaceRelativePath(ScriptFile scriptFile) => scriptFile.FilePath; From 9cbd9e631af6e416f5fcfa0ec5973c5bf1bbc313 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 12 May 2026 18:38:52 +0000 Subject: [PATCH 06/31] Fix Saved property XML docs wording Agent-Logs-Url: https://github.com/PowerShell/PowerShellEditorServices/sessions/3e87bc0c-ffcc-4ce4-9b08-22d248a3f5db Co-authored-by: JustinGrote <15258962+JustinGrote@users.noreply.github.com> --- src/PowerShellEditorServices/Extensions/EditorWorkspace.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PowerShellEditorServices/Extensions/EditorWorkspace.cs b/src/PowerShellEditorServices/Extensions/EditorWorkspace.cs index 19c5dac6a..2380019d6 100644 --- a/src/PowerShellEditorServices/Extensions/EditorWorkspace.cs +++ b/src/PowerShellEditorServices/Extensions/EditorWorkspace.cs @@ -23,7 +23,7 @@ internal EditorWorkspaceDocument(EditorWorkspace workspace, string path, bool sa public string Path { get; } /// - /// Gets whether the document has unsaved changes. + /// Gets whether the document is saved (has no unsaved changes). /// public bool Saved { get; } From 8afd2a56c32d9f228090c0187e8d7b6f2e13b64b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 12 May 2026 18:55:28 +0000 Subject: [PATCH 07/31] Fix ToString filename extraction for Windows paths Agent-Logs-Url: https://github.com/PowerShell/PowerShellEditorServices/sessions/6285ab7f-3d36-4174-84cd-57dcebf32386 Co-authored-by: JustinGrote <15258962+JustinGrote@users.noreply.github.com> --- src/PowerShellEditorServices/Extensions/EditorWorkspace.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/PowerShellEditorServices/Extensions/EditorWorkspace.cs b/src/PowerShellEditorServices/Extensions/EditorWorkspace.cs index 2380019d6..7b5d75b3f 100644 --- a/src/PowerShellEditorServices/Extensions/EditorWorkspace.cs +++ b/src/PowerShellEditorServices/Extensions/EditorWorkspace.cs @@ -33,7 +33,8 @@ internal EditorWorkspaceDocument(EditorWorkspace workspace, string path, bool sa /// The display name of this document. public override string ToString() { - string fileName = System.IO.Path.GetFileName(Path); + int fileNameStartIndex = System.Math.Max(Path.LastIndexOf('\\'), Path.LastIndexOf('/')) + 1; + string fileName = Path.Substring(fileNameStartIndex); return Saved ? fileName : fileName + " [Unsaved]"; } From 866c2aafc9500525b09a2247f0c1b368eeac4e3b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 12 May 2026 18:58:51 +0000 Subject: [PATCH 08/31] Harden ToString path handling for null and separators Agent-Logs-Url: https://github.com/PowerShell/PowerShellEditorServices/sessions/6285ab7f-3d36-4174-84cd-57dcebf32386 Co-authored-by: JustinGrote <15258962+JustinGrote@users.noreply.github.com> --- src/PowerShellEditorServices/Extensions/EditorWorkspace.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/PowerShellEditorServices/Extensions/EditorWorkspace.cs b/src/PowerShellEditorServices/Extensions/EditorWorkspace.cs index 7b5d75b3f..3f8b3639f 100644 --- a/src/PowerShellEditorServices/Extensions/EditorWorkspace.cs +++ b/src/PowerShellEditorServices/Extensions/EditorWorkspace.cs @@ -33,8 +33,10 @@ internal EditorWorkspaceDocument(EditorWorkspace workspace, string path, bool sa /// The display name of this document. public override string ToString() { - int fileNameStartIndex = System.Math.Max(Path.LastIndexOf('\\'), Path.LastIndexOf('/')) + 1; - string fileName = Path.Substring(fileNameStartIndex); + string documentPath = Path ?? string.Empty; + // Handle Windows and POSIX separators consistently across platforms. + int fileNameStartIndex = System.Math.Max(documentPath.LastIndexOf('\\'), documentPath.LastIndexOf('/')) + 1; + string fileName = documentPath.Substring(fileNameStartIndex); return Saved ? fileName : fileName + " [Unsaved]"; } From b781fd1420c37ebadaae7c3e62c71850ab55a706 Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Tue, 12 May 2026 12:53:15 -0700 Subject: [PATCH 09/31] Simplify methods --- .../Extensions/EditorWorkspace.cs | 44 +++++++------------ 1 file changed, 17 insertions(+), 27 deletions(-) diff --git a/src/PowerShellEditorServices/Extensions/EditorWorkspace.cs b/src/PowerShellEditorServices/Extensions/EditorWorkspace.cs index 3f8b3639f..54467594e 100644 --- a/src/PowerShellEditorServices/Extensions/EditorWorkspace.cs +++ b/src/PowerShellEditorServices/Extensions/EditorWorkspace.cs @@ -1,6 +1,9 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System.Collections.Generic; +using System.Linq; + namespace Microsoft.PowerShell.EditorServices.Extensions { /// @@ -27,19 +30,6 @@ internal EditorWorkspaceDocument(EditorWorkspace workspace, string path, bool sa /// public bool Saved { get; } - /// - /// Gets the display name of this document and unsaved status. - /// - /// The display name of this document. - public override string ToString() - { - string documentPath = Path ?? string.Empty; - // Handle Windows and POSIX separators consistently across platforms. - int fileNameStartIndex = System.Math.Max(documentPath.LastIndexOf('\\'), documentPath.LastIndexOf('/')) + 1; - string fileName = documentPath.Substring(fileNameStartIndex); - return Saved ? fileName : fileName + " [Unsaved]"; - } - /// /// Opens this document in the editor. /// @@ -54,6 +44,17 @@ public override string ToString() /// Closes this document in the editor. /// public void Close() => _workspace.CloseFile(Path); + + /// + /// Gets the display name of this document and unsaved status. + /// + /// The display name of this document. + public override string ToString() + { + string documentPath = Path ?? string.Empty; + string fileName = System.IO.Path.GetFileName(documentPath); + return Saved ? fileName : fileName + " [Unsaved]"; + } } /// @@ -84,20 +85,9 @@ public sealed class EditorWorkspace /// /// Get all currently open documents in the workspace. /// - public EditorWorkspaceDocument[] Documents - { - get - { - WorkspaceOpenDocument[] openDocuments = editorOperations.GetWorkspaceOpenDocuments(); - EditorWorkspaceDocument[] documents = new EditorWorkspaceDocument[openDocuments.Length]; - for (int i = 0; i < openDocuments.Length; i++) - { - documents[i] = new EditorWorkspaceDocument(this, openDocuments[i].Path, openDocuments[i].Saved); - } - - return documents; - } - } + public IEnumerable Documents => editorOperations + .GetWorkspaceOpenDocuments() + .Select(doc => new EditorWorkspaceDocument(this, doc.Path, doc.Saved)); #endregion From 29ae10f420df93f51591e56f9fac8b94a53f12bf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 12 May 2026 19:56:08 +0000 Subject: [PATCH 10/31] Use IsInMemory for workspace document saved state Agent-Logs-Url: https://github.com/PowerShell/PowerShellEditorServices/sessions/b2f2d9b7-95c7-413b-8a23-7845a964c98a Co-authored-by: JustinGrote <15258962+JustinGrote@users.noreply.github.com> --- .../Services/Extension/EditorOperationsService.cs | 2 +- .../Services/TextDocument/Handlers/TextDocumentHandler.cs | 1 - .../Services/TextDocument/ScriptFile.cs | 7 ------- 3 files changed, 1 insertion(+), 9 deletions(-) diff --git a/src/PowerShellEditorServices/Services/Extension/EditorOperationsService.cs b/src/PowerShellEditorServices/Services/Extension/EditorOperationsService.cs index 5260f3d39..ad812d07d 100644 --- a/src/PowerShellEditorServices/Services/Extension/EditorOperationsService.cs +++ b/src/PowerShellEditorServices/Services/Extension/EditorOperationsService.cs @@ -200,7 +200,7 @@ public async Task SaveFileAsync(string currentPath, string newSavePath) public WorkspaceOpenDocument[] GetWorkspaceOpenDocuments() => _workspaceService.GetOpenedFiles() - .Select(static scriptFile => new WorkspaceOpenDocument(scriptFile.FilePath, scriptFile.IsSaved)) + .Select(static scriptFile => new WorkspaceOpenDocument(scriptFile.FilePath, !scriptFile.IsInMemory)) .ToArray(); public string GetWorkspaceRelativePath(ScriptFile scriptFile) => _workspaceService.GetRelativePath(scriptFile); diff --git a/src/PowerShellEditorServices/Services/TextDocument/Handlers/TextDocumentHandler.cs b/src/PowerShellEditorServices/Services/TextDocument/Handlers/TextDocumentHandler.cs index c48c73889..7f982df3f 100644 --- a/src/PowerShellEditorServices/Services/TextDocument/Handlers/TextDocumentHandler.cs +++ b/src/PowerShellEditorServices/Services/TextDocument/Handlers/TextDocumentHandler.cs @@ -137,7 +137,6 @@ public override async Task Handle(DidSaveTextDocumentParams notification, await _remoteFileManagerService.SaveRemoteFileAsync(savedFile.FilePath).ConfigureAwait(false); } - savedFile.IsSaved = true; } return Unit.Value; } diff --git a/src/PowerShellEditorServices/Services/TextDocument/ScriptFile.cs b/src/PowerShellEditorServices/Services/TextDocument/ScriptFile.cs index f4986c1ed..fb6772d2b 100644 --- a/src/PowerShellEditorServices/Services/TextDocument/ScriptFile.cs +++ b/src/PowerShellEditorServices/Services/TextDocument/ScriptFile.cs @@ -55,11 +55,6 @@ internal sealed class ScriptFile /// public bool IsInMemory { get; } - /// - /// Gets or sets whether this file has no unsaved changes. - /// - public bool IsSaved { get; internal set; } - /// /// Gets a string containing the full contents of the file. /// @@ -142,7 +137,6 @@ internal ScriptFile( // SetFileContents() calls ParseFileContents() which initializes the rest of the properties. SetFileContents(textReader.ReadToEnd()); - IsSaved = !IsInMemory; References = new ReferenceTable(this); } @@ -370,7 +364,6 @@ public void ApplyChange(FileChange fileChange) // Parse the script again to be up-to-date ParseFileContents(); - IsSaved = false; References.TagAsChanged(); } From a568cf4e864f20e071efc3398002fd001c686199 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 12 May 2026 20:00:00 +0000 Subject: [PATCH 11/31] Clarify Saved semantics as file-backed state Agent-Logs-Url: https://github.com/PowerShell/PowerShellEditorServices/sessions/b2f2d9b7-95c7-413b-8a23-7845a964c98a Co-authored-by: JustinGrote <15258962+JustinGrote@users.noreply.github.com> --- src/PowerShellEditorServices/Extensions/EditorWorkspace.cs | 2 +- .../Extensions/IEditorOperations.cs | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/PowerShellEditorServices/Extensions/EditorWorkspace.cs b/src/PowerShellEditorServices/Extensions/EditorWorkspace.cs index 54467594e..bdd8d1db2 100644 --- a/src/PowerShellEditorServices/Extensions/EditorWorkspace.cs +++ b/src/PowerShellEditorServices/Extensions/EditorWorkspace.cs @@ -26,7 +26,7 @@ internal EditorWorkspaceDocument(EditorWorkspace workspace, string path, bool sa public string Path { get; } /// - /// Gets whether the document is saved (has no unsaved changes). + /// Gets whether the document is backed by a saved file path (not in-memory). /// public bool Saved { get; } diff --git a/src/PowerShellEditorServices/Extensions/IEditorOperations.cs b/src/PowerShellEditorServices/Extensions/IEditorOperations.cs index b3e629a0c..286478a55 100644 --- a/src/PowerShellEditorServices/Extensions/IEditorOperations.cs +++ b/src/PowerShellEditorServices/Extensions/IEditorOperations.cs @@ -14,8 +14,14 @@ internal WorkspaceOpenDocument(string path, bool saved) Saved = saved; } + /// + /// Gets the path or URI of the open document. + /// public string Path { get; } + /// + /// Gets whether the document is backed by a saved file path (not in-memory). + /// public bool Saved { get; } } From 945736e7bf617fcefac5954d1ddefc76c130aeeb Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Tue, 12 May 2026 13:04:40 -0700 Subject: [PATCH 12/31] Remove unnecessary newline --- .../Services/TextDocument/Handlers/TextDocumentHandler.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/PowerShellEditorServices/Services/TextDocument/Handlers/TextDocumentHandler.cs b/src/PowerShellEditorServices/Services/TextDocument/Handlers/TextDocumentHandler.cs index 7f982df3f..a02e7b884 100644 --- a/src/PowerShellEditorServices/Services/TextDocument/Handlers/TextDocumentHandler.cs +++ b/src/PowerShellEditorServices/Services/TextDocument/Handlers/TextDocumentHandler.cs @@ -136,7 +136,6 @@ public override async Task Handle(DidSaveTextDocumentParams notification, { await _remoteFileManagerService.SaveRemoteFileAsync(savedFile.FilePath).ConfigureAwait(false); } - } return Unit.Value; } From 9b19bc2aa52e5852848446f83b2afbeb457d95bd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 12 May 2026 20:10:14 +0000 Subject: [PATCH 13/31] Fix EditorWorkspaceTests for IEnumerable Documents Agent-Logs-Url: https://github.com/PowerShell/PowerShellEditorServices/sessions/2e354cc4-b0b9-4177-8ac1-8826e0921553 Co-authored-by: JustinGrote <15258962+JustinGrote@users.noreply.github.com> --- .../Extensions/EditorWorkspaceTests.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/PowerShellEditorServices.Test/Extensions/EditorWorkspaceTests.cs b/test/PowerShellEditorServices.Test/Extensions/EditorWorkspaceTests.cs index 1fec290f1..3c882d5bc 100644 --- a/test/PowerShellEditorServices.Test/Extensions/EditorWorkspaceTests.cs +++ b/test/PowerShellEditorServices.Test/Extensions/EditorWorkspaceTests.cs @@ -27,7 +27,7 @@ public void DocumentsReturnsOpenWorkspaceDocuments() EditorWorkspace workspace = new(editorOperations); - EditorWorkspaceDocument[] documents = workspace.Documents; + IEnumerable documents = workspace.Documents; Assert.Collection( documents, @@ -81,7 +81,7 @@ public void DocumentToStringReturnsFileNameAndSavedStatus() }; EditorWorkspace workspace = new(editorOperations); - EditorWorkspaceDocument[] documents = workspace.Documents; + IEnumerable documents = workspace.Documents; Assert.Collection( documents, @@ -102,7 +102,7 @@ public void DocumentSavedReturnsWorkspaceSavedState() }; EditorWorkspace workspace = new(editorOperations); - EditorWorkspaceDocument[] documents = workspace.Documents; + IEnumerable documents = workspace.Documents; Assert.Collection( documents, From b82b81ecacbdeef6f080934f1fa9f2b9d0f406e4 Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Tue, 12 May 2026 13:14:49 -0700 Subject: [PATCH 14/31] Switch to array and collection expressions --- .../Extensions/EditorWorkspace.cs | 7 ++++--- .../Extensions/EditorWorkspaceTests.cs | 10 +++++----- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/PowerShellEditorServices/Extensions/EditorWorkspace.cs b/src/PowerShellEditorServices/Extensions/EditorWorkspace.cs index bdd8d1db2..d1790ae90 100644 --- a/src/PowerShellEditorServices/Extensions/EditorWorkspace.cs +++ b/src/PowerShellEditorServices/Extensions/EditorWorkspace.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System.Collections.Generic; using System.Linq; namespace Microsoft.PowerShell.EditorServices.Extensions @@ -85,9 +84,11 @@ public sealed class EditorWorkspace /// /// Get all currently open documents in the workspace. /// - public IEnumerable Documents => editorOperations + public EditorWorkspaceDocument[] Documents => [.. + editorOperations .GetWorkspaceOpenDocuments() - .Select(doc => new EditorWorkspaceDocument(this, doc.Path, doc.Saved)); + .Select(doc => new EditorWorkspaceDocument(this, doc.Path, doc.Saved)) + ]; #endregion diff --git a/test/PowerShellEditorServices.Test/Extensions/EditorWorkspaceTests.cs b/test/PowerShellEditorServices.Test/Extensions/EditorWorkspaceTests.cs index 3c882d5bc..20e811e95 100644 --- a/test/PowerShellEditorServices.Test/Extensions/EditorWorkspaceTests.cs +++ b/test/PowerShellEditorServices.Test/Extensions/EditorWorkspaceTests.cs @@ -18,16 +18,16 @@ public void DocumentsReturnsOpenWorkspaceDocuments() { TestEditorOperations editorOperations = new() { - OpenDocuments = new[] - { + OpenDocuments = + [ new WorkspaceOpenDocument(@"C:\test\one.ps1", saved: true), new WorkspaceOpenDocument(@"C:\test\two.ps1", saved: true) - } + ] }; EditorWorkspace workspace = new(editorOperations); - IEnumerable documents = workspace.Documents; + EditorWorkspaceDocument[] documents = workspace.Documents; Assert.Collection( documents, @@ -49,7 +49,7 @@ public void DocumentOpenSaveAndCloseUseWorkspaceOperations() const string filePath = @"C:\test\file.ps1"; TestEditorOperations editorOperations = new() { - OpenDocuments = new[] { new WorkspaceOpenDocument(filePath, saved: true) } + OpenDocuments = [new WorkspaceOpenDocument(filePath, saved: true)] }; EditorWorkspace workspace = new(editorOperations); From dd0f79fd3ba0d486848dccc18f07ca9dffb6c3f5 Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Tue, 12 May 2026 13:43:36 -0700 Subject: [PATCH 15/31] Make EditorWorkspaceTests OS Independent --- .../Extensions/EditorWorkspaceTests.cs | 38 ++++++++++--------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/test/PowerShellEditorServices.Test/Extensions/EditorWorkspaceTests.cs b/test/PowerShellEditorServices.Test/Extensions/EditorWorkspaceTests.cs index 20e811e95..fb721fb51 100644 --- a/test/PowerShellEditorServices.Test/Extensions/EditorWorkspaceTests.cs +++ b/test/PowerShellEditorServices.Test/Extensions/EditorWorkspaceTests.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.IO; using System.Threading.Tasks; using Microsoft.PowerShell.EditorServices.Extensions; using Microsoft.PowerShell.EditorServices.Services.TextDocument; @@ -13,15 +14,20 @@ namespace PowerShellEditorServices.Test.Extensions [Trait("Category", "Extensions")] public class EditorWorkspaceTests { + private static readonly string WorkspacePath = Path.Combine("test"); + [Fact] public void DocumentsReturnsOpenWorkspaceDocuments() { + string firstPath = Path.Combine(WorkspacePath, "one.ps1"); + string secondPath = Path.Combine(WorkspacePath, "two.ps1"); + TestEditorOperations editorOperations = new() { OpenDocuments = [ - new WorkspaceOpenDocument(@"C:\test\one.ps1", saved: true), - new WorkspaceOpenDocument(@"C:\test\two.ps1", saved: true) + new WorkspaceOpenDocument(firstPath, saved: true), + new WorkspaceOpenDocument(secondPath, saved: true) ] }; @@ -33,12 +39,12 @@ public void DocumentsReturnsOpenWorkspaceDocuments() documents, document => { - Assert.Equal(@"C:\test\one.ps1", document.Path); + Assert.Equal(firstPath, document.Path); Assert.True(document.Saved); }, document => { - Assert.Equal(@"C:\test\two.ps1", document.Path); + Assert.Equal(secondPath, document.Path); Assert.True(document.Saved); }); } @@ -46,7 +52,7 @@ public void DocumentsReturnsOpenWorkspaceDocuments() [Fact] public void DocumentOpenSaveAndCloseUseWorkspaceOperations() { - const string filePath = @"C:\test\file.ps1"; + string filePath = Path.Combine(WorkspacePath, "file.ps1"); TestEditorOperations editorOperations = new() { OpenDocuments = [new WorkspaceOpenDocument(filePath, saved: true)] @@ -69,15 +75,14 @@ public void DocumentOpenSaveAndCloseUseWorkspaceOperations() [Fact] public void DocumentToStringReturnsFileNameAndSavedStatus() { - const string savedFilePath = @"C:\test\file.ps1"; - const string unsavedFilePath = @"C:\test\other.ps1"; + string savedFilePath = Path.Combine(WorkspacePath, "file.ps1"); + string unsavedFilePath = Path.Combine(WorkspacePath, "other.ps1"); TestEditorOperations editorOperations = new() { - OpenDocuments = new[] - { + OpenDocuments = [ new WorkspaceOpenDocument(savedFilePath, saved: true), new WorkspaceOpenDocument(unsavedFilePath, saved: false) - } + ] }; EditorWorkspace workspace = new(editorOperations); @@ -94,11 +99,10 @@ public void DocumentSavedReturnsWorkspaceSavedState() { TestEditorOperations editorOperations = new() { - OpenDocuments = new[] - { - new WorkspaceOpenDocument(@"C:\test\saved.ps1", saved: true), - new WorkspaceOpenDocument(@"C:\test\unsaved.ps1", saved: false) - } + OpenDocuments = [ + new WorkspaceOpenDocument(Path.Combine(WorkspacePath, "saved.ps1"), saved: true), + new WorkspaceOpenDocument(Path.Combine(WorkspacePath, "unsaved.ps1"), saved: false) + ] }; EditorWorkspace workspace = new(editorOperations); @@ -118,9 +122,9 @@ private sealed class TestEditorOperations : IEditorOperations public Task GetEditorContextAsync() => Task.FromResult(default(EditorContext)); - public string GetWorkspacePath() => @"C:\test"; + public string GetWorkspacePath() => WorkspacePath; - public string[] GetWorkspacePaths() => new[] { @"C:\test" }; + public string[] GetWorkspacePaths() => [WorkspacePath]; public WorkspaceOpenDocument[] GetWorkspaceOpenDocuments() => OpenDocuments; From 38dd80121d2f3187654e4e4ce176beafefa7ce99 Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Tue, 12 May 2026 15:41:59 -0700 Subject: [PATCH 16/31] Fixup Open/Unsaved file identification --- .../Extension/EditorOperationsService.cs | 9 ++- .../Handlers/TextDocumentHandler.cs | 8 ++- .../Services/TextDocument/ScriptFile.cs | 20 +++++-- .../EditorOperationsServiceTests.cs | 55 +++++++++++++++++++ 4 files changed, 83 insertions(+), 9 deletions(-) create mode 100644 test/PowerShellEditorServices.Test/Extensions/EditorOperationsServiceTests.cs diff --git a/src/PowerShellEditorServices/Services/Extension/EditorOperationsService.cs b/src/PowerShellEditorServices/Services/Extension/EditorOperationsService.cs index ad812d07d..9c3e0c4ff 100644 --- a/src/PowerShellEditorServices/Services/Extension/EditorOperationsService.cs +++ b/src/PowerShellEditorServices/Services/Extension/EditorOperationsService.cs @@ -199,9 +199,12 @@ public async Task SaveFileAsync(string currentPath, string newSavePath) public string[] GetWorkspacePaths() => _workspaceService.WorkspacePaths.ToArray(); public WorkspaceOpenDocument[] GetWorkspaceOpenDocuments() - => _workspaceService.GetOpenedFiles() - .Select(static scriptFile => new WorkspaceOpenDocument(scriptFile.FilePath, !scriptFile.IsInMemory)) - .ToArray(); + => [.. + _workspaceService + .GetOpenedFiles() + .Where(static scriptFile => scriptFile.IsOpen) + .Select(static scriptFile => new WorkspaceOpenDocument(scriptFile.FilePath, !scriptFile.IsInMemory)) + ]; public string GetWorkspaceRelativePath(ScriptFile scriptFile) => _workspaceService.GetRelativePath(scriptFile); diff --git a/src/PowerShellEditorServices/Services/TextDocument/Handlers/TextDocumentHandler.cs b/src/PowerShellEditorServices/Services/TextDocument/Handlers/TextDocumentHandler.cs index a02e7b884..5ad70905c 100644 --- a/src/PowerShellEditorServices/Services/TextDocument/Handlers/TextDocumentHandler.cs +++ b/src/PowerShellEditorServices/Services/TextDocument/Handlers/TextDocumentHandler.cs @@ -111,10 +111,11 @@ public override Task Handle(DidCloseTextDocumentParams notification, Cance { fileToClose.IsOpen = false; - // If the file watcher is supported, only close in-memory files when this + // If the file watcher is supported, only close non-file-backed documents when this // notification is triggered. This lets us keep workspace files open so we can scan // for references. When a file is deleted, the file watcher will close the file. - if (!_isFileWatcherSupported || fileToClose.IsInMemory) + bool isBackedByFile = notification.TextDocument.Uri.ToUri().IsFile; + if (!_isFileWatcherSupported || !isBackedByFile) { _workspaceService.CloseFile(fileToClose); } @@ -132,6 +133,9 @@ public override async Task Handle(DidSaveTextDocumentParams notification, if (savedFile != null) { + // On a save, untitled files will remain in memory, so this won't change for those + savedFile.IsInMemory = savedFile.IsUntitled; + if (_remoteFileManagerService.IsUnderRemoteTempPath(savedFile.FilePath)) { await _remoteFileManagerService.SaveRemoteFileAsync(savedFile.FilePath).ConfigureAwait(false); diff --git a/src/PowerShellEditorServices/Services/TextDocument/ScriptFile.cs b/src/PowerShellEditorServices/Services/TextDocument/ScriptFile.cs index fb6772d2b..96912d751 100644 --- a/src/PowerShellEditorServices/Services/TextDocument/ScriptFile.cs +++ b/src/PowerShellEditorServices/Services/TextDocument/ScriptFile.cs @@ -51,9 +51,14 @@ internal sealed class ScriptFile /// /// Gets a boolean that determines whether this file is - /// in-memory or not (either unsaved or non-file content). + /// in-memory or not (either unsaved or non-file content) aka "dirty" /// - public bool IsInMemory { get; } + public bool IsInMemory { get; internal set; } + + /// + /// Getter that returns if the document is not backed by a saved file path (not in-memory). + /// + public bool IsUntitled => !DocumentUri?.ToUri().IsFile ?? false; /// /// Gets a string containing the full contents of the file. @@ -127,11 +132,15 @@ internal ScriptFile( // so that other operations know it's untitled/in-memory // and don't think that it's a relative path // on the file system. - IsInMemory = !docUri.ToUri().IsFile; + DocumentUri = docUri; + + // Initial state of document. Untitled files are in memory by definition, otherwise files start non-dirty on a filesystem + IsInMemory = IsUntitled; + FilePath = IsInMemory ? docUri.ToString() : docUri.GetFileSystemPath(); - DocumentUri = docUri; + IsAnalysisEnabled = true; this.powerShellVersion = powerShellVersion; @@ -365,6 +374,9 @@ public void ApplyChange(FileChange fileChange) // Parse the script again to be up-to-date ParseFileContents(); References.TagAsChanged(); + + // Flag the script as modified + IsInMemory = true; } /// diff --git a/test/PowerShellEditorServices.Test/Extensions/EditorOperationsServiceTests.cs b/test/PowerShellEditorServices.Test/Extensions/EditorOperationsServiceTests.cs new file mode 100644 index 000000000..e679bdeb1 --- /dev/null +++ b/test/PowerShellEditorServices.Test/Extensions/EditorOperationsServiceTests.cs @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.IO; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.PowerShell.EditorServices.Extensions; +using Microsoft.PowerShell.EditorServices.Services; +using Microsoft.PowerShell.EditorServices.Services.Extension; +using Microsoft.PowerShell.EditorServices.Services.TextDocument; +using OmniSharp.Extensions.LanguageServer.Protocol; +using Xunit; + +namespace PowerShellEditorServices.Test.Extensions +{ + [Trait("Category", "Extensions")] + public class EditorOperationsServiceTests + { + [Fact] + public void GetWorkspaceOpenDocumentsReturnsOnlyOpenDocumentsAndCurrentInMemoryState() + { + WorkspaceService workspaceService = new(NullLoggerFactory.Instance); + + ScriptFile openSaved = CreateFileBuffer(workspaceService, "open-saved.ps1"); + openSaved.IsOpen = true; + openSaved.IsInMemory = false; + + ScriptFile openUnsaved = CreateFileBuffer(workspaceService, "open-unsaved.ps1"); + openUnsaved.IsOpen = true; + openUnsaved.IsInMemory = true; + + ScriptFile closed = CreateFileBuffer(workspaceService, "closed.ps1"); + closed.IsOpen = false; + closed.IsInMemory = false; + + EditorOperationsService editorOperationsService = new( + psesHost: null, + workspaceService, + languageServer: null); + + WorkspaceOpenDocument[] documents = editorOperationsService.GetWorkspaceOpenDocuments(); + + Assert.Equal(2, documents.Length); + Assert.Contains(documents, static document => document.Path.EndsWith("open-saved.ps1") && document.Saved); + Assert.Contains(documents, static document => document.Path.EndsWith("open-unsaved.ps1") && !document.Saved); + Assert.DoesNotContain(documents, static document => document.Path.EndsWith("closed.ps1")); + } + + private static ScriptFile CreateFileBuffer(WorkspaceService workspaceService, string fileName) + { + string filePath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"), fileName); + return workspaceService.GetFileBuffer(DocumentUri.FromFileSystemPath(filePath), initialBuffer: string.Empty); + } + } +} From 8220f533a366db467d44e65e470059d09f352b85 Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Tue, 12 May 2026 16:04:34 -0700 Subject: [PATCH 17/31] Try to fix flaky vim test --- test/vim-simple-test.vim | 14 +++++++++++++- test/vim-test.vim | 14 +++++++++++++- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/test/vim-simple-test.vim b/test/vim-simple-test.vim index fe0adda74..4a670b8db 100644 --- a/test/vim-simple-test.vim +++ b/test/vim-simple-test.vim @@ -1,6 +1,18 @@ let s:suite = themis#suite('pses') let s:assert = themis#helper('assert') +function s:wait_for_diagnostics(bufname, expected) + let l:attempts = 20 + while l:attempts > 0 + if getbufvar(a:bufname, 'LanguageClient_statusLineDiagnosticsCounts') == a:expected + return + endif + + execute 'sleep 500m' + let l:attempts -= 1 + endwhile +endfunction + function s:suite.before() let l:pses_path = g:repo_root . '/module' let g:LanguageClient_serverCommands = { @@ -33,7 +45,7 @@ function s:suite.analyzes_powershell_file() call s:assert.equal(getbufvar(l:bufinfo.bufnr, '&filetype'), 'ps1') execute 'LanguageClientStart' - execute 'sleep' 5 + call s:wait_for_diagnostics(l:bufinfo.name, {'E': 0, 'W': 1, 'H': 0, 'I': 0}) call s:assert.equal(getbufvar(l:bufinfo.name, 'LanguageClient_isServerRunning'), 1) call s:assert.equal(getbufvar(l:bufinfo.name, 'LanguageClient_projectRoot'), g:repo_root) call s:assert.equal(getbufvar(l:bufinfo.name, 'LanguageClient_statusLineDiagnosticsCounts'), {'E': 0, 'W': 1, 'H': 0, 'I': 0}) diff --git a/test/vim-test.vim b/test/vim-test.vim index 3d94d174c..4fc5cfcd5 100644 --- a/test/vim-test.vim +++ b/test/vim-test.vim @@ -1,6 +1,18 @@ let s:suite = themis#suite('pses') let s:assert = themis#helper('assert') +function s:wait_for_diagnostics(bufname, expected) + let l:attempts = 20 + while l:attempts > 0 + if getbufvar(a:bufname, 'LanguageClient_statusLineDiagnosticsCounts') == a:expected + return + endif + + execute 'sleep 500m' + let l:attempts -= 1 + endwhile +endfunction + function s:suite.before() let l:pses_path = g:repo_root . '/module' let g:LanguageClient_serverCommands = { @@ -37,7 +49,7 @@ function s:suite.analyzes_powershell_file() call s:assert.equal(getbufvar(l:bufinfo.bufnr, '&filetype'), 'ps1') execute 'LanguageClientStart' - execute 'sleep' 5 + call s:wait_for_diagnostics(l:bufinfo.name, {'E': 0, 'W': 1, 'H': 0, 'I': 0}) call s:assert.equal(getbufvar(l:bufinfo.name, 'LanguageClient_isServerRunning'), 1) call s:assert.equal(getbufvar(l:bufinfo.name, 'LanguageClient_projectRoot'), g:repo_root) call s:assert.equal(getbufvar(l:bufinfo.name, 'LanguageClient_statusLineDiagnosticsCounts'), {'E': 0, 'W': 1, 'H': 0, 'I': 0}) From 2a0df442dc302b6689a254fb7bbdf9124bc40278 Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Tue, 12 May 2026 17:07:14 -0700 Subject: [PATCH 18/31] Simplify API abstraction to just the existing WorkspaceOpenDocument --- .../Extensions/EditorWorkspace.cs | 59 +------------------ .../Extensions/IEditorOperations.cs | 24 +++++--- .../Extensions/EditorWorkspaceTests.cs | 12 ++-- 3 files changed, 23 insertions(+), 72 deletions(-) diff --git a/src/PowerShellEditorServices/Extensions/EditorWorkspace.cs b/src/PowerShellEditorServices/Extensions/EditorWorkspace.cs index d1790ae90..a19c3d538 100644 --- a/src/PowerShellEditorServices/Extensions/EditorWorkspace.cs +++ b/src/PowerShellEditorServices/Extensions/EditorWorkspace.cs @@ -5,57 +5,6 @@ namespace Microsoft.PowerShell.EditorServices.Extensions { - /// - /// A document currently open in the editor workspace. - /// - public sealed class EditorWorkspaceDocument - { - private readonly EditorWorkspace _workspace; - - internal EditorWorkspaceDocument(EditorWorkspace workspace, string path, bool saved) - { - _workspace = workspace; - Path = path; - Saved = saved; - } - - /// - /// Gets the path of the document. - /// - public string Path { get; } - - /// - /// Gets whether the document is backed by a saved file path (not in-memory). - /// - public bool Saved { get; } - - /// - /// Opens this document in the editor. - /// - public void Open() => _workspace.OpenFile(Path); - - /// - /// Saves this document in the editor. - /// - public void Save() => _workspace.SaveFile(Path); - - /// - /// Closes this document in the editor. - /// - public void Close() => _workspace.CloseFile(Path); - - /// - /// Gets the display name of this document and unsaved status. - /// - /// The display name of this document. - public override string ToString() - { - string documentPath = Path ?? string.Empty; - string fileName = System.IO.Path.GetFileName(documentPath); - return Saved ? fileName : fileName + " [Unsaved]"; - } - } - /// /// Provides a PowerShell-facing API which allows scripts to /// interact with the editor's workspace. @@ -84,11 +33,7 @@ public sealed class EditorWorkspace /// /// Get all currently open documents in the workspace. /// - public EditorWorkspaceDocument[] Documents => [.. - editorOperations - .GetWorkspaceOpenDocuments() - .Select(doc => new EditorWorkspaceDocument(this, doc.Path, doc.Saved)) - ]; + public WorkspaceOpenDocument[] Documents => [.. editorOperations.GetWorkspaceOpenDocuments()]; #endregion @@ -138,6 +83,7 @@ public sealed class EditorWorkspace /// The path to the file to be closed. [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD002:Avoid problematic synchronous waits", Justification = "Supporting synchronous API.")] public void CloseFile(string filePath) => editorOperations.CloseFileAsync(filePath).Wait(); + public void CloseFile(WorkspaceOpenDocument document) => CloseFile(document.Path); /// /// Saves an open file in the workspace. @@ -145,6 +91,7 @@ public sealed class EditorWorkspace /// The path to the file to be saved. [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD002:Avoid problematic synchronous waits", Justification = "Supporting synchronous API.")] public void SaveFile(string filePath) => editorOperations.SaveFileAsync(filePath).Wait(); + public void SaveFile(WorkspaceOpenDocument document) => SaveFile(document.Path); /// /// Saves a file with a new name AKA a copy. diff --git a/src/PowerShellEditorServices/Extensions/IEditorOperations.cs b/src/PowerShellEditorServices/Extensions/IEditorOperations.cs index 286478a55..7fb39b22c 100644 --- a/src/PowerShellEditorServices/Extensions/IEditorOperations.cs +++ b/src/PowerShellEditorServices/Extensions/IEditorOperations.cs @@ -3,26 +3,32 @@ using System.Threading.Tasks; using Microsoft.PowerShell.EditorServices.Services.TextDocument; +#nullable enable namespace Microsoft.PowerShell.EditorServices.Extensions { - internal readonly struct WorkspaceOpenDocument + public readonly struct WorkspaceOpenDocument(string path, bool saved) { - internal WorkspaceOpenDocument(string path, bool saved) - { - Path = path; - Saved = saved; - } - /// /// Gets the path or URI of the open document. /// - public string Path { get; } + public string Path { get; } = path; /// /// Gets whether the document is backed by a saved file path (not in-memory). /// - public bool Saved { get; } + public bool Saved { get; } = saved; + + /// + /// Gets the display name of this document and unsaved status. + /// + /// The display name of this document. + public override string ToString() + { + string documentPath = Path ?? string.Empty; + string fileName = System.IO.Path.GetFileName(documentPath); + return Saved ? fileName : fileName + " [Unsaved]"; + } } /// diff --git a/test/PowerShellEditorServices.Test/Extensions/EditorWorkspaceTests.cs b/test/PowerShellEditorServices.Test/Extensions/EditorWorkspaceTests.cs index fb721fb51..ab027b1e9 100644 --- a/test/PowerShellEditorServices.Test/Extensions/EditorWorkspaceTests.cs +++ b/test/PowerShellEditorServices.Test/Extensions/EditorWorkspaceTests.cs @@ -33,7 +33,7 @@ public void DocumentsReturnsOpenWorkspaceDocuments() EditorWorkspace workspace = new(editorOperations); - EditorWorkspaceDocument[] documents = workspace.Documents; + WorkspaceOpenDocument[] documents = workspace.Documents; Assert.Collection( documents, @@ -50,7 +50,7 @@ public void DocumentsReturnsOpenWorkspaceDocuments() } [Fact] - public void DocumentOpenSaveAndCloseUseWorkspaceOperations() + public void DocumentSaveAndCloseUseWorkspaceOperations() { string filePath = Path.Combine(WorkspacePath, "file.ps1"); TestEditorOperations editorOperations = new() @@ -59,15 +59,13 @@ public void DocumentOpenSaveAndCloseUseWorkspaceOperations() }; EditorWorkspace workspace = new(editorOperations); - EditorWorkspaceDocument document = Assert.Single(workspace.Documents); + WorkspaceOpenDocument document = Assert.Single(workspace.Documents); - document.Open(); document.Save(); document.Close(); Assert.Collection( editorOperations.Calls, - call => Assert.Equal("OpenFile:" + filePath, call), call => Assert.Equal("SaveFile:" + filePath, call), call => Assert.Equal("CloseFile:" + filePath, call)); } @@ -86,7 +84,7 @@ public void DocumentToStringReturnsFileNameAndSavedStatus() }; EditorWorkspace workspace = new(editorOperations); - IEnumerable documents = workspace.Documents; + IEnumerable documents = workspace.Documents; Assert.Collection( documents, @@ -106,7 +104,7 @@ public void DocumentSavedReturnsWorkspaceSavedState() }; EditorWorkspace workspace = new(editorOperations); - IEnumerable documents = workspace.Documents; + IEnumerable documents = workspace.Documents; Assert.Collection( documents, From 9d5106349f1c81f3a95f354bd67a1fdf1dac2e9a Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Tue, 12 May 2026 17:09:02 -0700 Subject: [PATCH 19/31] Remove dangling using --- src/PowerShellEditorServices/Extensions/EditorWorkspace.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/PowerShellEditorServices/Extensions/EditorWorkspace.cs b/src/PowerShellEditorServices/Extensions/EditorWorkspace.cs index a19c3d538..209d6b091 100644 --- a/src/PowerShellEditorServices/Extensions/EditorWorkspace.cs +++ b/src/PowerShellEditorServices/Extensions/EditorWorkspace.cs @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System.Linq; - namespace Microsoft.PowerShell.EditorServices.Extensions { /// From cfeba0e85f3336536db5852843ce0221b0415247 Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Tue, 12 May 2026 17:24:55 -0700 Subject: [PATCH 20/31] Fix Tests --- .../Extensions/EditorWorkspaceTests.cs | 21 ------------------- 1 file changed, 21 deletions(-) diff --git a/test/PowerShellEditorServices.Test/Extensions/EditorWorkspaceTests.cs b/test/PowerShellEditorServices.Test/Extensions/EditorWorkspaceTests.cs index ab027b1e9..673d864d5 100644 --- a/test/PowerShellEditorServices.Test/Extensions/EditorWorkspaceTests.cs +++ b/test/PowerShellEditorServices.Test/Extensions/EditorWorkspaceTests.cs @@ -49,27 +49,6 @@ public void DocumentsReturnsOpenWorkspaceDocuments() }); } - [Fact] - public void DocumentSaveAndCloseUseWorkspaceOperations() - { - string filePath = Path.Combine(WorkspacePath, "file.ps1"); - TestEditorOperations editorOperations = new() - { - OpenDocuments = [new WorkspaceOpenDocument(filePath, saved: true)] - }; - - EditorWorkspace workspace = new(editorOperations); - WorkspaceOpenDocument document = Assert.Single(workspace.Documents); - - document.Save(); - document.Close(); - - Assert.Collection( - editorOperations.Calls, - call => Assert.Equal("SaveFile:" + filePath, call), - call => Assert.Equal("CloseFile:" + filePath, call)); - } - [Fact] public void DocumentToStringReturnsFileNameAndSavedStatus() { From 9bba5bfe3cdac44a15212480d4591c547857844d Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Wed, 13 May 2026 08:52:46 -0700 Subject: [PATCH 21/31] Additional comments and logging --- .../Services/TextDocument/ScriptFile.cs | 3 +++ .../Services/Workspace/WorkspaceService.cs | 9 ++++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/PowerShellEditorServices/Services/TextDocument/ScriptFile.cs b/src/PowerShellEditorServices/Services/TextDocument/ScriptFile.cs index 96912d751..b8d60ce08 100644 --- a/src/PowerShellEditorServices/Services/TextDocument/ScriptFile.cs +++ b/src/PowerShellEditorServices/Services/TextDocument/ScriptFile.cs @@ -110,6 +110,9 @@ public Token[] ScriptTokens internal ReferenceTable References { get; } + /// + /// Indicates whether the file is currently open in the editor. PSES may open files for analysis that aren't actually visible in the editor. + /// internal bool IsOpen { get; set; } #endregion diff --git a/src/PowerShellEditorServices/Services/Workspace/WorkspaceService.cs b/src/PowerShellEditorServices/Services/Workspace/WorkspaceService.cs index efd7f82d7..ac6799238 100644 --- a/src/PowerShellEditorServices/Services/Workspace/WorkspaceService.cs +++ b/src/PowerShellEditorServices/Services/Workspace/WorkspaceService.cs @@ -302,7 +302,14 @@ public void CloseFile(ScriptFile scriptFile) Validate.IsNotNull(nameof(scriptFile), scriptFile); string keyName = GetFileKey(scriptFile.DocumentUri); - workspaceFiles.TryRemove(keyName, out ScriptFile _); + if (workspaceFiles.TryRemove(keyName, out ScriptFile _)) + { + logger.LogDebug("Closed file: " + scriptFile.DocumentUri); + } + else + { + logger.LogWarning("Tried to close file that was not open: " + scriptFile.DocumentUri); + } } /// From 25e22404b203ef9ff5d91e8572dd238ffb7d4a63 Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Wed, 13 May 2026 09:34:50 -0700 Subject: [PATCH 22/31] Simplify IsUntitled getter (even tho I'm pretty sure this will break it again) Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../Services/TextDocument/ScriptFile.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/PowerShellEditorServices/Services/TextDocument/ScriptFile.cs b/src/PowerShellEditorServices/Services/TextDocument/ScriptFile.cs index b8d60ce08..9cff31d41 100644 --- a/src/PowerShellEditorServices/Services/TextDocument/ScriptFile.cs +++ b/src/PowerShellEditorServices/Services/TextDocument/ScriptFile.cs @@ -56,9 +56,10 @@ internal sealed class ScriptFile public bool IsInMemory { get; internal set; } /// - /// Getter that returns if the document is not backed by a saved file path (not in-memory). + /// Gets a value indicating whether the document URI is not a file:// URI + /// (for example, an untitled: URI). /// - public bool IsUntitled => !DocumentUri?.ToUri().IsFile ?? false; + public bool IsUntitled => !DocumentUri.ToUri().IsFile; /// /// Gets a string containing the full contents of the file. From 7e2f5d4056b6cb207e43cb62ae3b14da595e7fce Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Wed, 13 May 2026 09:36:02 -0700 Subject: [PATCH 23/31] Remove unnecessary collection expression Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/PowerShellEditorServices/Extensions/EditorWorkspace.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PowerShellEditorServices/Extensions/EditorWorkspace.cs b/src/PowerShellEditorServices/Extensions/EditorWorkspace.cs index 209d6b091..f3c40d74a 100644 --- a/src/PowerShellEditorServices/Extensions/EditorWorkspace.cs +++ b/src/PowerShellEditorServices/Extensions/EditorWorkspace.cs @@ -31,7 +31,7 @@ public sealed class EditorWorkspace /// /// Get all currently open documents in the workspace. /// - public WorkspaceOpenDocument[] Documents => [.. editorOperations.GetWorkspaceOpenDocuments()]; + public WorkspaceOpenDocument[] Documents => editorOperations.GetWorkspaceOpenDocuments(); #endregion From 8a167cbdf2950f9bf5ce26226035e7aa9f78e0e0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 13 May 2026 16:36:07 +0000 Subject: [PATCH 24/31] Fix file-backed checks to use IsUntitled Agent-Logs-Url: https://github.com/PowerShell/PowerShellEditorServices/sessions/25fb4a4a-04f5-471e-93fa-82a0a9cdcd91 Co-authored-by: JustinGrote <15258962+JustinGrote@users.noreply.github.com> --- .../Services/TextDocument/Handlers/CompletionHandler.cs | 2 +- .../Services/Workspace/WorkspaceService.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PowerShellEditorServices/Services/TextDocument/Handlers/CompletionHandler.cs b/src/PowerShellEditorServices/Services/TextDocument/Handlers/CompletionHandler.cs index c3d7a39c5..8dbb8f798 100644 --- a/src/PowerShellEditorServices/Services/TextDocument/Handlers/CompletionHandler.cs +++ b/src/PowerShellEditorServices/Services/TextDocument/Handlers/CompletionHandler.cs @@ -366,7 +366,7 @@ private CompletionItem CreateProviderItemCompletion( if (textToBeReplaced.IndexOf(PSScriptRootVariable, StringComparison.OrdinalIgnoreCase) is int variableIndex and not -1 && System.IO.Path.GetDirectoryName(scriptFile.FilePath) is string scriptFolder and not "" && completionText.IndexOf(scriptFolder, StringComparison.OrdinalIgnoreCase) is int pathIndex and not -1 - && !scriptFile.IsInMemory) + && !scriptFile.IsUntitled) { completionText = completionText .Remove(pathIndex, scriptFolder.Length) diff --git a/src/PowerShellEditorServices/Services/Workspace/WorkspaceService.cs b/src/PowerShellEditorServices/Services/Workspace/WorkspaceService.cs index ac6799238..9b721387a 100644 --- a/src/PowerShellEditorServices/Services/Workspace/WorkspaceService.cs +++ b/src/PowerShellEditorServices/Services/Workspace/WorkspaceService.cs @@ -319,7 +319,7 @@ public void CloseFile(ScriptFile scriptFile) public string GetRelativePath(ScriptFile scriptFile) { Uri fileUri = scriptFile.DocumentUri.ToUri(); - if (!scriptFile.IsInMemory) + if (!scriptFile.IsUntitled) { // Support calculating out-of-workspace relative paths in the common case of a // single workspace folder. Otherwise try to get the matching folder. From f14c10df69e20c05470c8dc0c35a110443edd510 Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Wed, 13 May 2026 09:37:45 -0700 Subject: [PATCH 25/31] Avoid extra recompilation for IsUntitled Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../Services/TextDocument/Handlers/TextDocumentHandler.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PowerShellEditorServices/Services/TextDocument/Handlers/TextDocumentHandler.cs b/src/PowerShellEditorServices/Services/TextDocument/Handlers/TextDocumentHandler.cs index 5ad70905c..ba2e9242b 100644 --- a/src/PowerShellEditorServices/Services/TextDocument/Handlers/TextDocumentHandler.cs +++ b/src/PowerShellEditorServices/Services/TextDocument/Handlers/TextDocumentHandler.cs @@ -114,7 +114,7 @@ public override Task Handle(DidCloseTextDocumentParams notification, Cance // If the file watcher is supported, only close non-file-backed documents when this // notification is triggered. This lets us keep workspace files open so we can scan // for references. When a file is deleted, the file watcher will close the file. - bool isBackedByFile = notification.TextDocument.Uri.ToUri().IsFile; + bool isBackedByFile = !fileToClose.IsUntitled; if (!_isFileWatcherSupported || !isBackedByFile) { _workspaceService.CloseFile(fileToClose); From e105ee67f3cad386f57b71cfdadf88784d1f2db6 Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Wed, 13 May 2026 09:38:15 -0700 Subject: [PATCH 26/31] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../EditorOperationsServiceTests.cs | 78 +++++++++---------- 1 file changed, 39 insertions(+), 39 deletions(-) diff --git a/test/PowerShellEditorServices.Test/Extensions/EditorOperationsServiceTests.cs b/test/PowerShellEditorServices.Test/Extensions/EditorOperationsServiceTests.cs index e679bdeb1..bf702ee03 100644 --- a/test/PowerShellEditorServices.Test/Extensions/EditorOperationsServiceTests.cs +++ b/test/PowerShellEditorServices.Test/Extensions/EditorOperationsServiceTests.cs @@ -13,43 +13,43 @@ namespace PowerShellEditorServices.Test.Extensions { - [Trait("Category", "Extensions")] - public class EditorOperationsServiceTests - { - [Fact] - public void GetWorkspaceOpenDocumentsReturnsOnlyOpenDocumentsAndCurrentInMemoryState() - { - WorkspaceService workspaceService = new(NullLoggerFactory.Instance); - - ScriptFile openSaved = CreateFileBuffer(workspaceService, "open-saved.ps1"); - openSaved.IsOpen = true; - openSaved.IsInMemory = false; - - ScriptFile openUnsaved = CreateFileBuffer(workspaceService, "open-unsaved.ps1"); - openUnsaved.IsOpen = true; - openUnsaved.IsInMemory = true; - - ScriptFile closed = CreateFileBuffer(workspaceService, "closed.ps1"); - closed.IsOpen = false; - closed.IsInMemory = false; - - EditorOperationsService editorOperationsService = new( - psesHost: null, - workspaceService, - languageServer: null); - - WorkspaceOpenDocument[] documents = editorOperationsService.GetWorkspaceOpenDocuments(); - - Assert.Equal(2, documents.Length); - Assert.Contains(documents, static document => document.Path.EndsWith("open-saved.ps1") && document.Saved); - Assert.Contains(documents, static document => document.Path.EndsWith("open-unsaved.ps1") && !document.Saved); - Assert.DoesNotContain(documents, static document => document.Path.EndsWith("closed.ps1")); - } - - private static ScriptFile CreateFileBuffer(WorkspaceService workspaceService, string fileName) - { - string filePath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"), fileName); - return workspaceService.GetFileBuffer(DocumentUri.FromFileSystemPath(filePath), initialBuffer: string.Empty); - } - } + [Trait("Category", "Extensions")] + public class EditorOperationsServiceTests + { + [Fact] + public void GetWorkspaceOpenDocumentsReturnsOnlyOpenDocumentsAndCurrentInMemoryState() + { + WorkspaceService workspaceService = new(NullLoggerFactory.Instance); + + ScriptFile openSaved = CreateFileBuffer(workspaceService, "open-saved.ps1"); + openSaved.IsOpen = true; + openSaved.IsInMemory = false; + + ScriptFile openUnsaved = CreateFileBuffer(workspaceService, "open-unsaved.ps1"); + openUnsaved.IsOpen = true; + openUnsaved.IsInMemory = true; + + ScriptFile closed = CreateFileBuffer(workspaceService, "closed.ps1"); + closed.IsOpen = false; + closed.IsInMemory = false; + + EditorOperationsService editorOperationsService = new( + psesHost: null, + workspaceService, + languageServer: null); + + WorkspaceOpenDocument[] documents = editorOperationsService.GetWorkspaceOpenDocuments(); + + Assert.Equal(2, documents.Length); + Assert.Contains(documents, static document => document.Path.EndsWith("open-saved.ps1") && document.Saved); + Assert.Contains(documents, static document => document.Path.EndsWith("open-unsaved.ps1") && !document.Saved); + Assert.DoesNotContain(documents, static document => document.Path.EndsWith("closed.ps1")); + } + + private static ScriptFile CreateFileBuffer(WorkspaceService workspaceService, string fileName) + { + string filePath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"), fileName); + return workspaceService.GetFileBuffer(DocumentUri.FromFileSystemPath(filePath), initialBuffer: string.Empty); + } + } } From cfead40b037ea950b0089b48db3debc88a1b46c8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 13 May 2026 16:43:44 +0000 Subject: [PATCH 27/31] Add transition coverage for workspace open document saved state Agent-Logs-Url: https://github.com/PowerShell/PowerShellEditorServices/sessions/f6b6da25-a6a1-4158-9738-3a65a7e3008e Co-authored-by: JustinGrote <15258962+JustinGrote@users.noreply.github.com> --- .../EditorOperationsServiceTests.cs | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/test/PowerShellEditorServices.Test/Extensions/EditorOperationsServiceTests.cs b/test/PowerShellEditorServices.Test/Extensions/EditorOperationsServiceTests.cs index bf702ee03..5b5b887c7 100644 --- a/test/PowerShellEditorServices.Test/Extensions/EditorOperationsServiceTests.cs +++ b/test/PowerShellEditorServices.Test/Extensions/EditorOperationsServiceTests.cs @@ -46,6 +46,43 @@ public void GetWorkspaceOpenDocumentsReturnsOnlyOpenDocumentsAndCurrentInMemoryS Assert.DoesNotContain(documents, static document => document.Path.EndsWith("closed.ps1")); } + [Fact] + public void GetWorkspaceOpenDocumentsTracksEditedAndUntitledSaveStates() + { + WorkspaceService workspaceService = new(NullLoggerFactory.Instance); + + ScriptFile openSaved = CreateFileBuffer(workspaceService, "open-saved.ps1"); + openSaved.IsOpen = true; + + ScriptFile openUntitled = workspaceService.GetFileBuffer("untitled:Untitled-1", initialBuffer: string.Empty); + openUntitled.IsOpen = true; + + EditorOperationsService editorOperationsService = new( + psesHost: null, + workspaceService, + languageServer: null); + + WorkspaceOpenDocument[] initialDocuments = editorOperationsService.GetWorkspaceOpenDocuments(); + Assert.Contains(initialDocuments, static document => document.Path.EndsWith("open-saved.ps1") && document.Saved); + Assert.Contains(initialDocuments, static document => document.Path.StartsWith("untitled:", StringComparison.Ordinal) && !document.Saved); + + openSaved.ApplyChange(new FileChange + { + IsReload = true, + InsertString = "Set-StrictMode -Version Latest" + }); + + WorkspaceOpenDocument[] editedDocuments = editorOperationsService.GetWorkspaceOpenDocuments(); + Assert.Contains(editedDocuments, static document => document.Path.EndsWith("open-saved.ps1") && !document.Saved); + + openSaved.IsInMemory = openSaved.IsUntitled; + openUntitled.IsInMemory = openUntitled.IsUntitled; + + WorkspaceOpenDocument[] savedDocuments = editorOperationsService.GetWorkspaceOpenDocuments(); + Assert.Contains(savedDocuments, static document => document.Path.EndsWith("open-saved.ps1") && document.Saved); + Assert.Contains(savedDocuments, static document => document.Path.StartsWith("untitled:", StringComparison.Ordinal) && !document.Saved); + } + private static ScriptFile CreateFileBuffer(WorkspaceService workspaceService, string fileName) { string filePath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"), fileName); From 45f719c46a37acc8939e89229c89afc95a1b467b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 13 May 2026 16:44:34 +0000 Subject: [PATCH 28/31] Refine workspace document transition test semantics Agent-Logs-Url: https://github.com/PowerShell/PowerShellEditorServices/sessions/f6b6da25-a6a1-4158-9738-3a65a7e3008e Co-authored-by: JustinGrote <15258962+JustinGrote@users.noreply.github.com> --- .../Extensions/EditorOperationsServiceTests.cs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/test/PowerShellEditorServices.Test/Extensions/EditorOperationsServiceTests.cs b/test/PowerShellEditorServices.Test/Extensions/EditorOperationsServiceTests.cs index 5b5b887c7..30c2bfdd5 100644 --- a/test/PowerShellEditorServices.Test/Extensions/EditorOperationsServiceTests.cs +++ b/test/PowerShellEditorServices.Test/Extensions/EditorOperationsServiceTests.cs @@ -68,15 +68,18 @@ public void GetWorkspaceOpenDocumentsTracksEditedAndUntitledSaveStates() openSaved.ApplyChange(new FileChange { - IsReload = true, + Line = 1, + Offset = 1, + EndLine = 1, + EndOffset = 1, InsertString = "Set-StrictMode -Version Latest" }); WorkspaceOpenDocument[] editedDocuments = editorOperationsService.GetWorkspaceOpenDocuments(); Assert.Contains(editedDocuments, static document => document.Path.EndsWith("open-saved.ps1") && !document.Saved); - openSaved.IsInMemory = openSaved.IsUntitled; - openUntitled.IsInMemory = openUntitled.IsUntitled; + SimulateSaveReset(openSaved); + SimulateSaveReset(openUntitled); WorkspaceOpenDocument[] savedDocuments = editorOperationsService.GetWorkspaceOpenDocuments(); Assert.Contains(savedDocuments, static document => document.Path.EndsWith("open-saved.ps1") && document.Saved); @@ -88,5 +91,10 @@ private static ScriptFile CreateFileBuffer(WorkspaceService workspaceService, st string filePath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"), fileName); return workspaceService.GetFileBuffer(DocumentUri.FromFileSystemPath(filePath), initialBuffer: string.Empty); } + + private static void SimulateSaveReset(ScriptFile scriptFile) + { + scriptFile.IsInMemory = scriptFile.IsUntitled; + } } } From 9c3ac22d5b7afa8e88566de23acd7239dbfdaaa6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 13 May 2026 16:45:14 +0000 Subject: [PATCH 29/31] Rename test helper to clarify saved-state intent Agent-Logs-Url: https://github.com/PowerShell/PowerShellEditorServices/sessions/f6b6da25-a6a1-4158-9738-3a65a7e3008e Co-authored-by: JustinGrote <15258962+JustinGrote@users.noreply.github.com> --- .../Extensions/EditorOperationsServiceTests.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/PowerShellEditorServices.Test/Extensions/EditorOperationsServiceTests.cs b/test/PowerShellEditorServices.Test/Extensions/EditorOperationsServiceTests.cs index 30c2bfdd5..19767a095 100644 --- a/test/PowerShellEditorServices.Test/Extensions/EditorOperationsServiceTests.cs +++ b/test/PowerShellEditorServices.Test/Extensions/EditorOperationsServiceTests.cs @@ -78,8 +78,8 @@ public void GetWorkspaceOpenDocumentsTracksEditedAndUntitledSaveStates() WorkspaceOpenDocument[] editedDocuments = editorOperationsService.GetWorkspaceOpenDocuments(); Assert.Contains(editedDocuments, static document => document.Path.EndsWith("open-saved.ps1") && !document.Saved); - SimulateSaveReset(openSaved); - SimulateSaveReset(openUntitled); + MarkAsSaved(openSaved); + MarkAsSaved(openUntitled); WorkspaceOpenDocument[] savedDocuments = editorOperationsService.GetWorkspaceOpenDocuments(); Assert.Contains(savedDocuments, static document => document.Path.EndsWith("open-saved.ps1") && document.Saved); @@ -92,7 +92,7 @@ private static ScriptFile CreateFileBuffer(WorkspaceService workspaceService, st return workspaceService.GetFileBuffer(DocumentUri.FromFileSystemPath(filePath), initialBuffer: string.Empty); } - private static void SimulateSaveReset(ScriptFile scriptFile) + private static void MarkAsSaved(ScriptFile scriptFile) { scriptFile.IsInMemory = scriptFile.IsUntitled; } From 605e82cdbb61c065ab86363c00ba1b6906f8b693 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 13 May 2026 16:45:55 +0000 Subject: [PATCH 30/31] Assert edited content in open-document transition test Agent-Logs-Url: https://github.com/PowerShell/PowerShellEditorServices/sessions/f6b6da25-a6a1-4158-9738-3a65a7e3008e Co-authored-by: JustinGrote <15258962+JustinGrote@users.noreply.github.com> --- .../Extensions/EditorOperationsServiceTests.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/test/PowerShellEditorServices.Test/Extensions/EditorOperationsServiceTests.cs b/test/PowerShellEditorServices.Test/Extensions/EditorOperationsServiceTests.cs index 19767a095..a43982b72 100644 --- a/test/PowerShellEditorServices.Test/Extensions/EditorOperationsServiceTests.cs +++ b/test/PowerShellEditorServices.Test/Extensions/EditorOperationsServiceTests.cs @@ -74,6 +74,7 @@ public void GetWorkspaceOpenDocumentsTracksEditedAndUntitledSaveStates() EndOffset = 1, InsertString = "Set-StrictMode -Version Latest" }); + Assert.Contains("Set-StrictMode -Version Latest", openSaved.Contents, StringComparison.Ordinal); WorkspaceOpenDocument[] editedDocuments = editorOperationsService.GetWorkspaceOpenDocuments(); Assert.Contains(editedDocuments, static document => document.Path.EndsWith("open-saved.ps1") && !document.Saved); From 384b313cb98b640f6b08fd72376da0a608f3ed4b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 13 May 2026 20:18:42 +0000 Subject: [PATCH 31/31] Use expression-bodied helper to satisfy IDE0022 Agent-Logs-Url: https://github.com/PowerShell/PowerShellEditorServices/sessions/c6f63c96-d7c5-4f03-9391-a32f918383a6 Co-authored-by: JustinGrote <15258962+JustinGrote@users.noreply.github.com> --- .../Extensions/EditorOperationsServiceTests.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/test/PowerShellEditorServices.Test/Extensions/EditorOperationsServiceTests.cs b/test/PowerShellEditorServices.Test/Extensions/EditorOperationsServiceTests.cs index a43982b72..45ef4e538 100644 --- a/test/PowerShellEditorServices.Test/Extensions/EditorOperationsServiceTests.cs +++ b/test/PowerShellEditorServices.Test/Extensions/EditorOperationsServiceTests.cs @@ -93,9 +93,6 @@ private static ScriptFile CreateFileBuffer(WorkspaceService workspaceService, st return workspaceService.GetFileBuffer(DocumentUri.FromFileSystemPath(filePath), initialBuffer: string.Empty); } - private static void MarkAsSaved(ScriptFile scriptFile) - { - scriptFile.IsInMemory = scriptFile.IsUntitled; - } + private static void MarkAsSaved(ScriptFile scriptFile) => scriptFile.IsInMemory = scriptFile.IsUntitled; } }