From efb6c2252a1696d1cc124a8765714fdf772de2d0 Mon Sep 17 00:00:00 2001
From: Boulanger <Boulanger Adrien>
Date: Mon, 18 Mar 2024 15:09:21 +0100
Subject: [PATCH 1/5] Fix unsafe call to Update_Sources

Update_Sources has for precondition "not Self.Are_Sources_Loaded"
Thus the sources must always be invalidated before or it will
raise an exception and corrupt the contexts.

Add a test.

eng/ide/ada_language_server#1272
---
 .../ada/lsp-ada_handlers-project_loading.adb  |   9 +
 source/ada/lsp-ada_handlers.adb               |  12 +
 .../foo-bar1.ads                              |   3 +
 .../foo.ads                                   |   3 +
 .../main.adb                                  |   6 +
 .../test.gpr                                  |   3 +
 .../test.json                                 | 792 ++++++++++++++++++
 .../test.yaml                                 |   1 +
 8 files changed, 829 insertions(+)
 create mode 100644 testsuite/ada_lsp/Did_Rename_Files.context_corruption/foo-bar1.ads
 create mode 100644 testsuite/ada_lsp/Did_Rename_Files.context_corruption/foo.ads
 create mode 100644 testsuite/ada_lsp/Did_Rename_Files.context_corruption/main.adb
 create mode 100644 testsuite/ada_lsp/Did_Rename_Files.context_corruption/test.gpr
 create mode 100644 testsuite/ada_lsp/Did_Rename_Files.context_corruption/test.json
 create mode 100644 testsuite/ada_lsp/Did_Rename_Files.context_corruption/test.yaml

diff --git a/source/ada/lsp-ada_handlers-project_loading.adb b/source/ada/lsp-ada_handlers-project_loading.adb
index e6db1fb1b..304e6d430 100644
--- a/source/ada/lsp-ada_handlers-project_loading.adb
+++ b/source/ada/lsp-ada_handlers-project_loading.adb
@@ -423,6 +423,11 @@ package body LSP.Ada_Handlers.Project_Loading is
             Build_Path  => Project_Environment.Build_Path,
             Environment => Environment);
 
+         if Self.Project_Tree.Are_Sources_Loaded then
+            --  Update_Sources can't be called when the sources are already
+            --  loaded
+            Self.Project_Tree.Invalidate_Sources;
+         end if;
          Self.Project_Tree.Update_Sources (With_Runtime => True);
 
       exception
@@ -687,6 +692,10 @@ package body LSP.Ada_Handlers.Project_Loading is
          Project => Project,
          Context => GPR2.Context.Empty);
 
+      if Self.Project_Tree.Are_Sources_Loaded then
+         --  Update_Sources can't be called when the sources are already loaded
+         Self.Project_Tree.Invalidate_Sources;
+      end if;
       Self.Project_Tree.Update_Sources (With_Runtime => True);
 
    exception
diff --git a/source/ada/lsp-ada_handlers.adb b/source/ada/lsp-ada_handlers.adb
index 064dea57c..bda17393d 100644
--- a/source/ada/lsp-ada_handlers.adb
+++ b/source/ada/lsp-ada_handlers.adb
@@ -2295,6 +2295,10 @@ package body LSP.Ada_Handlers is
 
       --  New sources were created on this project, so recompute its view
 
+      if Self.Project_Tree.Are_Sources_Loaded then
+         --  Update_Sources can't be called when the sources are already loaded
+         Self.Project_Tree.Invalidate_Sources;
+      end if;
       Self.Project_Tree.Update_Sources (With_Runtime => True);
 
       --  For each created file of Value.files:
@@ -2346,6 +2350,10 @@ package body LSP.Ada_Handlers is
 
       --  Some project sources were deleted, so recompute its view
 
+      if Self.Project_Tree.Are_Sources_Loaded then
+         --  Update_Sources can't be called when the sources are already loaded
+         Self.Project_Tree.Invalidate_Sources;
+      end if;
       Self.Project_Tree.Update_Sources (With_Runtime => True);
 
       --  For each delete file of Value.files:
@@ -2461,6 +2469,10 @@ package body LSP.Ada_Handlers is
 
       --  Some project sources were renamed, so recompute its view
 
+      if Self.Project_Tree.Are_Sources_Loaded then
+         --  Update_Sources can't be called when the sources are already loaded
+         Self.Project_Tree.Invalidate_Sources;
+      end if;
       Self.Project_Tree.Update_Sources (With_Runtime => True);
 
       --  For each oldUri of Value.files:
diff --git a/testsuite/ada_lsp/Did_Rename_Files.context_corruption/foo-bar1.ads b/testsuite/ada_lsp/Did_Rename_Files.context_corruption/foo-bar1.ads
new file mode 100644
index 000000000..fa090612f
--- /dev/null
+++ b/testsuite/ada_lsp/Did_Rename_Files.context_corruption/foo-bar1.ads
@@ -0,0 +1,3 @@
+package Foo.Bar1 is
+   procedure N is null;
+end Foo.Bar1;
diff --git a/testsuite/ada_lsp/Did_Rename_Files.context_corruption/foo.ads b/testsuite/ada_lsp/Did_Rename_Files.context_corruption/foo.ads
new file mode 100644
index 000000000..633f971fe
--- /dev/null
+++ b/testsuite/ada_lsp/Did_Rename_Files.context_corruption/foo.ads
@@ -0,0 +1,3 @@
+package Foo is
+   procedure Do_Nothing is null;
+end Foo;
diff --git a/testsuite/ada_lsp/Did_Rename_Files.context_corruption/main.adb b/testsuite/ada_lsp/Did_Rename_Files.context_corruption/main.adb
new file mode 100644
index 000000000..49e2d1028
--- /dev/null
+++ b/testsuite/ada_lsp/Did_Rename_Files.context_corruption/main.adb
@@ -0,0 +1,6 @@
+with Foo.Bar1;
+
+procedure Main is
+begin
+   Foo.Bar1.N;
+end Main;
diff --git a/testsuite/ada_lsp/Did_Rename_Files.context_corruption/test.gpr b/testsuite/ada_lsp/Did_Rename_Files.context_corruption/test.gpr
new file mode 100644
index 000000000..13539987d
--- /dev/null
+++ b/testsuite/ada_lsp/Did_Rename_Files.context_corruption/test.gpr
@@ -0,0 +1,3 @@
+project Test is
+   for Main use ("main.adb");
+end Test;
diff --git a/testsuite/ada_lsp/Did_Rename_Files.context_corruption/test.json b/testsuite/ada_lsp/Did_Rename_Files.context_corruption/test.json
new file mode 100644
index 000000000..4d57ee14e
--- /dev/null
+++ b/testsuite/ada_lsp/Did_Rename_Files.context_corruption/test.json
@@ -0,0 +1,792 @@
+[
+   {
+      "comment": [
+         "Test the context after a DidRenameFiles notification. ",
+         "The references requests should work before and after."
+      ]
+   },
+   {
+      "start": {
+         "cmd": [
+            "${ALS}"
+         ]
+      }
+   },
+   {
+      "send": {
+         "request": {
+            "jsonrpc": "2.0",
+            "id": 1,
+            "method": "initialize",
+            "params": {
+               "processId": 257038,
+               "rootUri": "$URI{.}",
+               "capabilities": {
+                  "workspace": {
+                     "applyEdit": true,
+                     "workspaceEdit": {
+                        "documentChanges": true,
+                        "resourceOperations": [
+                           "rename"
+                        ]
+                     },
+                     "fileOperations": {
+                        "didRename": true
+                     }
+                  },
+                  "textDocument": {
+                     "synchronization": {},
+                     "completion": {
+                        "dynamicRegistration": true,
+                        "completionItem": {
+                           "snippetSupport": false,
+                           "documentationFormat": [
+                              "plaintext",
+                              "markdown"
+                           ],
+                           "resolveSupport": {
+                              "properties": [
+                                 "detail",
+                                 "documentation"
+                              ]
+                           }
+                        }
+                     },
+                     "hover": {},
+                     "signatureHelp": {},
+                     "declaration": {},
+                     "definition": {},
+                     "typeDefinition": {},
+                     "implementation": {},
+                     "documentSymbol": {
+                        "hierarchicalDocumentSymbolSupport": true
+                     },
+                     "formatting": {
+                        "dynamicRegistration": false
+                     },
+                     "rangeFormatting": {
+                        "dynamicRegistration": false
+                     },
+                     "onTypeFormatting": {
+                        "dynamicRegistration": false
+                     },
+                     "foldingRange": {
+                        "lineFoldingOnly": true
+                     }
+                  },
+                  "experimental": {
+                     "advanced_refactorings": [
+                        "add_parameter"
+                     ]
+                  }
+               }
+            }
+         },
+         "wait": [
+            {
+               "id": 1,
+               "result": {
+                  "capabilities": {
+                     "textDocumentSync": 2,
+                     "completionProvider": {
+                        "triggerCharacters": [
+                           ".",
+                           ",",
+                           "'",
+                           "("
+                        ],
+                        "resolveProvider": true
+                     },
+                     "renameProvider": {
+                        "prepareProvider": true
+                     }
+                  }
+               }
+            }
+         ]
+      }
+   },
+   {
+      "send": {
+         "request": {
+            "jsonrpc": "2.0",
+            "method": "initialized"
+         },
+         "wait": []
+      }
+   },
+   {
+      "send": {
+         "request": {
+            "jsonrpc": "2.0",
+            "method": "workspace/didChangeConfiguration",
+            "params": {
+               "settings": {
+                  "ada": {
+                     "projectFile": "$URI{test.gpr}",
+                     "scenarioVariables": {},
+                     "defaultCharset": "ISO-8859-1",
+                     "enableDiagnostics": true,
+                     "followSymlinks": false,
+                     "documentationStyle": "gnat",
+                     "namedNotationThreshold": 3,
+                     "foldComments": false
+                  }
+               }
+            }
+         },
+         "wait": []
+      }
+   },
+   {
+      "send": {
+         "request": {
+            "jsonrpc": "2.0",
+            "method": "textDocument/didOpen",
+            "params": {
+               "textDocument": {
+                  "uri": "$URI{foo-bar1.ads}",
+                  "languageId": "Ada",
+                  "version": 0,
+                  "text": "package Foo.Bar1 is\n   procedure N is null;\nend Foo.Bar1;\n"
+               }
+            }
+         },
+         "wait": []
+      }
+   },
+   {
+      "send": {
+         "request": {
+            "jsonrpc": "2.0",
+            "method": "textDocument/didOpen",
+            "params": {
+               "textDocument": {
+                  "uri": "$URI{main.adb}",
+                  "languageId": "Ada",
+                  "version": 0,
+                  "text": "with Foo.Bar1;\n\nprocedure Main is\n   begin\n   Foo.Bar1.N;\nend Main;\n"
+               }
+            }
+         },
+         "wait": []
+      }
+   },
+   {
+      "send": {
+         "request": {
+            "jsonrpc": "2.0",
+            "id": 7,
+            "method": "textDocument/references",
+            "params": {
+               "textDocument": {
+                  "uri": "$URI{foo-bar1.ads}"
+               },
+               "position": {
+                  "line": 1,
+                  "character": 13
+               },
+               "context": {
+                  "includeDeclaration": true
+               }
+            }
+         },
+         "wait": [
+            {
+               "id": 7,
+               "result": [
+                  {
+                     "uri": "$URI{foo-bar1.ads}",
+                     "range": {
+                        "start": {
+                           "line": 1,
+                           "character": 13
+                        },
+                        "end": {
+                           "line": 1,
+                           "character": 14
+                        }
+                     },
+                     "alsKind": [
+                        "reference"
+                     ]
+                  },
+                  {
+                     "uri": "$URI{main.adb}",
+                     "range": {
+                        "start": {
+                           "line": 4,
+                           "character": 12
+                        },
+                        "end": {
+                           "line": 4,
+                           "character": 13
+                        }
+                     },
+                     "alsKind": [
+                        "call"
+                     ]
+                  }
+               ]
+            }
+         ]
+      }
+   },
+   {
+      "send": {
+         "request": {
+            "jsonrpc": "2.0",
+            "id": 11,
+            "method": "textDocument/rename",
+            "params": {
+               "textDocument": {
+                  "uri": "$URI{foo-bar1.ads}"
+               },
+               "position": {
+                  "line": 0,
+                  "character": 12
+               },
+               "newName": "Bar2"
+            }
+         },
+         "wait": [
+            {
+               "id": 11,
+               "result": {
+                  "documentChanges": [
+                     {
+                        "textDocument": {
+                           "uri": "$URI{foo-bar1.ads}",
+                           "version": 1
+                        },
+                        "edits": [
+                           {
+                              "range": {
+                                 "start": {
+                                    "line": 0,
+                                    "character": 12
+                                 },
+                                 "end": {
+                                    "line": 0,
+                                    "character": 16
+                                 }
+                              },
+                              "newText": "Bar2",
+                              "annotationId": ""
+                           },
+                           {
+                              "range": {
+                                 "start": {
+                                    "line": 2,
+                                    "character": 8
+                                 },
+                                 "end": {
+                                    "line": 2,
+                                    "character": 12
+                                 }
+                              },
+                              "newText": "Bar2",
+                              "annotationId": ""
+                           }
+                        ]
+                     },
+                     {
+                        "textDocument": {
+                           "uri": "$URI{main.adb}",
+                           "version": 1
+                        },
+                        "edits": [
+                           {
+                              "range": {
+                                 "start": {
+                                    "line": 0,
+                                    "character": 9
+                                 },
+                                 "end": {
+                                    "line": 0,
+                                    "character": 13
+                                 }
+                              },
+                              "newText": "Bar2",
+                              "annotationId": ""
+                           },
+                           {
+                              "range": {
+                                 "start": {
+                                    "line": 4,
+                                    "character": 7
+                                 },
+                                 "end": {
+                                    "line": 4,
+                                    "character": 11
+                                 }
+                              },
+                              "newText": "Bar2",
+                              "annotationId": ""
+                           }
+                        ]
+                     },
+                     {
+                        "kind": "rename",
+                        "oldUri": "$URI{foo-bar1.ads}",
+                        "newUri": "$URI{foo-bar2.ads}"
+                     }
+                  ]
+               }
+            }
+         ]
+      }
+   },
+   {
+      "send": {
+         "request": {
+            "jsonrpc": "2.0",
+            "method": "textDocument/didChange",
+            "params": {
+               "textDocument": {
+                  "uri": "$URI{foo-bar1.ads}",
+                  "version": 1
+               },
+               "contentChanges": [
+                  {
+                     "range": {
+                        "start": {
+                           "line": 2,
+                           "character": 8
+                        },
+                        "end": {
+                           "line": 2,
+                           "character": 12
+                        }
+                     },
+                     "text": ""
+                  }
+               ]
+            }
+         },
+         "wait": []
+      }
+   },
+   {
+      "send": {
+         "request": {
+            "jsonrpc": "2.0",
+            "method": "textDocument/didChange",
+            "params": {
+               "textDocument": {
+                  "uri": "$URI{foo-bar1.ads}",
+                  "version": 2
+               },
+               "contentChanges": [
+                  {
+                     "range": {
+                        "start": {
+                           "line": 2,
+                           "character": 8
+                        },
+                        "end": {
+                           "line": 2,
+                           "character": 8
+                        }
+                     },
+                     "text": "Bar2"
+                  }
+               ]
+            }
+         },
+         "wait": []
+      }
+   },
+   {
+      "send": {
+         "request": {
+            "jsonrpc": "2.0",
+            "method": "textDocument/didChange",
+            "params": {
+               "textDocument": {
+                  "uri": "$URI{foo-bar1.ads}",
+                  "version": 3
+               },
+               "contentChanges": [
+                  {
+                     "range": {
+                        "start": {
+                           "line": 0,
+                           "character": 12
+                        },
+                        "end": {
+                           "line": 0,
+                           "character": 16
+                        }
+                     },
+                     "text": ""
+                  }
+               ]
+            }
+         },
+         "wait": []
+      }
+   },
+   {
+      "send": {
+         "request": {
+            "jsonrpc": "2.0",
+            "method": "textDocument/didChange",
+            "params": {
+               "textDocument": {
+                  "uri": "$URI{foo-bar1.ads}",
+                  "version": 4
+               },
+               "contentChanges": [
+                  {
+                     "range": {
+                        "start": {
+                           "line": 0,
+                           "character": 12
+                        },
+                        "end": {
+                           "line": 0,
+                           "character": 12
+                        }
+                     },
+                     "text": "Bar2"
+                  }
+               ]
+            }
+         },
+         "wait": []
+      }
+   },
+   {
+      "send": {
+         "request": {
+            "jsonrpc": "2.0",
+            "method": "textDocument/didOpen",
+            "params": {
+               "textDocument": {
+                  "uri": "$URI{main.adb}",
+                  "languageId": "Ada",
+                  "version": 0,
+                  "text": "with Foo.Bar1;\n\nprocedure Main is\nbegin\n   Foo.Bar1.N;\nend Main;\n"
+               }
+            }
+         },
+         "wait": []
+      }
+   },
+   {
+      "send": {
+         "request": {
+            "jsonrpc": "2.0",
+            "method": "textDocument/didChange",
+            "params": {
+               "textDocument": {
+                  "uri": "$URI{main.adb}",
+                  "version": 1
+               },
+               "contentChanges": [
+                  {
+                     "range": {
+                        "start": {
+                           "line": 4,
+                           "character": 7
+                        },
+                        "end": {
+                           "line": 4,
+                           "character": 11
+                        }
+                     },
+                     "text": ""
+                  }
+               ]
+            }
+         },
+         "wait": []
+      }
+   },
+   {
+      "send": {
+         "request": {
+            "jsonrpc": "2.0",
+            "method": "textDocument/didChange",
+            "params": {
+               "textDocument": {
+                  "uri": "$URI{main.adb}",
+                  "version": 2
+               },
+               "contentChanges": [
+                  {
+                     "range": {
+                        "start": {
+                           "line": 4,
+                           "character": 7
+                        },
+                        "end": {
+                           "line": 4,
+                           "character": 7
+                        }
+                     },
+                     "text": "Bar2"
+                  }
+               ]
+            }
+         },
+         "wait": []
+      }
+   },
+   {
+      "send": {
+         "request": {
+            "jsonrpc": "2.0",
+            "method": "textDocument/didChange",
+            "params": {
+               "textDocument": {
+                  "uri": "$URI{main.adb}",
+                  "version": 3
+               },
+               "contentChanges": [
+                  {
+                     "range": {
+                        "start": {
+                           "line": 0,
+                           "character": 9
+                        },
+                        "end": {
+                           "line": 0,
+                           "character": 13
+                        }
+                     },
+                     "text": ""
+                  }
+               ]
+            }
+         },
+         "wait": []
+      }
+   },
+   {
+      "send": {
+         "request": {
+            "jsonrpc": "2.0",
+            "method": "textDocument/didChange",
+            "params": {
+               "textDocument": {
+                  "uri": "$URI{main.adb}",
+                  "version": 4
+               },
+               "contentChanges": [
+                  {
+                     "range": {
+                        "start": {
+                           "line": 0,
+                           "character": 9
+                        },
+                        "end": {
+                           "line": 0,
+                           "character": 9
+                        }
+                     },
+                     "text": "Bar2"
+                  }
+               ]
+            }
+         },
+         "wait": []
+      }
+   },
+   {
+      "send": {
+         "request": {
+            "jsonrpc": "2.0",
+            "method": "textDocument/didClose",
+            "params": {
+               "textDocument": {
+                  "uri": "$URI{foo-bar1.ads}"
+               }
+            }
+         },
+         "wait": []
+      }
+   },
+   {
+      "send": {
+         "request": {
+            "jsonrpc": "2.0",
+            "method": "textDocument/didOpen",
+            "params": {
+               "textDocument": {
+                  "uri": "$URI{foo-bar2.ads}",
+                  "languageId": "Ada",
+                  "version": 0,
+                  "text": "package Foo.Bar2 is\n   procedure N is null;\nend Foo.Bar2;\n"
+               }
+            }
+         },
+         "wait": []
+      }
+   },
+   {
+      "send": {
+         "request": {
+            "jsonrpc": "2.0",
+            "id": 12,
+            "method": "workspace/executeCommand",
+            "params": {
+               "arguments": [],
+               "command": "als-reload-project"
+            }
+         },
+         "wait": []
+      }
+   },
+   {
+      "send": {
+         "request": {
+            "jsonrpc": "2.0",
+            "method": "textDocument/didClose",
+            "params": {
+               "textDocument": {
+                  "uri": "$URI{foo-bar1.ads}"
+               }
+            }
+         },
+         "wait": []
+      }
+   },
+   {
+      "send": {
+         "request": {
+            "jsonrpc": "2.0",
+            "method": "textDocument/didOpen",
+            "params": {
+               "textDocument": {
+                  "uri": "$URI{foo-bar2.ads}",
+                  "languageId": "Ada",
+                  "version": 0,
+                  "text": "package Foo.Bar2 is\n   procedure N is null;\nend Foo.Bar2;\n"
+               }
+            }
+         },
+         "wait": []
+      }
+   },
+   {
+      "send": {
+         "request": {
+            "jsonrpc": "2.0",
+            "method": "workspace/didRenameFiles",
+            "params": {
+               "files": [
+                  {
+                     "oldUri": "$URI{foo-bar1.ads}",
+                     "newUri": "$URI{foo-bar2.ads}"
+                  }
+               ]
+            }
+         },
+         "wait": []
+      }
+   },
+   {
+      "send": {
+         "request": {
+            "jsonrpc": "2.0",
+            "id": 22,
+            "method": "textDocument/references",
+            "params": {
+               "textDocument": {
+                  "uri": "$URI{foo-bar2.ads}"
+               },
+               "position": {
+                  "line": 1,
+                  "character": 13
+               },
+               "context": {
+                  "includeDeclaration": true
+               }
+            }
+         },
+         "wait": [
+            {
+               "id": 22,
+               "result": [
+                  {
+                     "uri": "$URI{foo-bar2.ads}",
+                     "range": {
+                        "start": {
+                           "line": 1,
+                           "character": 13
+                        },
+                        "end": {
+                           "line": 1,
+                           "character": 14
+                        }
+                     },
+                     "alsKind": [
+                        "reference"
+                     ]
+                  },
+                  {
+                     "uri": "$URI{main.adb}",
+                     "range": {
+                        "start": {
+                           "line": 4,
+                           "character": 12
+                        },
+                        "end": {
+                           "line": 4,
+                           "character": 13
+                        }
+                     },
+                     "alsKind": [
+                        "call"
+                     ]
+                  }
+               ]
+            }
+         ]
+      }
+   },
+   {
+      "send": {
+         "request": {
+            "jsonrpc": "2.0",
+            "method": "textDocument/didClose",
+            "params": {
+               "textDocument": {
+                  "uri": "$URI{foo-bar2.ads}"
+               }
+            }
+         },
+         "wait": [
+            {
+               "method": "textDocument/publishDiagnostics",
+               "params": {
+                  "uri": "$URI{foo-bar2.ads}",
+                  "diagnostics": []
+               }
+            }
+         ]
+      }
+   },
+   {
+      "send": {
+         "request": {
+            "jsonrpc": "2.0",
+            "id": 24,
+            "method": "shutdown"
+         },
+         "wait": [
+            {
+               "id": 24,
+               "result": null
+            }
+         ]
+      }
+   },
+   {
+      "stop": {
+         "exit_code": 0
+      }
+   }
+]
diff --git a/testsuite/ada_lsp/Did_Rename_Files.context_corruption/test.yaml b/testsuite/ada_lsp/Did_Rename_Files.context_corruption/test.yaml
new file mode 100644
index 000000000..947a313ae
--- /dev/null
+++ b/testsuite/ada_lsp/Did_Rename_Files.context_corruption/test.yaml
@@ -0,0 +1 @@
+title: 'Did_Rename_Files.context_corruption'

From a706d1d7383be63c01ba3c998e5554148ea148f1 Mon Sep 17 00:00:00 2001
From: Elie Richa <richa@adacore.com>
Date: Mon, 11 Mar 2024 16:14:02 +0000
Subject: [PATCH 2/5] Make test restore file content

---
 .../vscode/ada/test/suite/general/extension.test.ts  | 12 +++++++++++-
 1 file changed, 11 insertions(+), 1 deletion(-)

diff --git a/integration/vscode/ada/test/suite/general/extension.test.ts b/integration/vscode/ada/test/suite/general/extension.test.ts
index 8a3e55f47..e11f57486 100644
--- a/integration/vscode/ada/test/suite/general/extension.test.ts
+++ b/integration/vscode/ada/test/suite/general/extension.test.ts
@@ -5,6 +5,7 @@ import { getProjectFile, getObjectDir } from '../../../src/helpers';
 import { assertEqualToFileContent, activate } from '../utils';
 
 import * as vscode from 'vscode';
+import { readFileSync, writeFileSync } from 'fs';
 
 suite('Extensions Test Suite', function () {
     // Make sure the extension is activated
@@ -38,6 +39,7 @@ suite('Extensions Test Suite', function () {
             ];
             const folder = vscode.workspace.workspaceFolders[0].uri;
             const fileUri = vscode.Uri.joinPath(folder, 'src', 'test_subprogram_box.adb');
+            const contentBefore = readFileSync(fileUri.fsPath);
             const expectedUri = vscode.Uri.joinPath(folder, 'src', 'test_subprogram_box.expected');
 
             for (const cursorPos of cursorPositions) {
@@ -47,8 +49,16 @@ suite('Extensions Test Suite', function () {
                 await vscode.commands.executeCommand('ada.subprogramBox');
             }
             const editorText = vscode.window.activeTextEditor?.document.getText() ?? '';
+            vscode.window.activeTextEditor?.hide();
 
-            assertEqualToFileContent(editorText, expectedUri);
+            try {
+                assertEqualToFileContent(editorText, expectedUri);
+            } finally {
+                /**
+                 * Restore the old file content
+                 */
+                writeFileSync(fileUri.fsPath, contentBefore);
+            }
         } else {
             throw new Error('No workspace folder found for the specified URI');
         }

From 97135ad1d06db79ae41cbfeea93badd1c5104e14 Mon Sep 17 00:00:00 2001
From: Elie Richa <richa@adacore.com>
Date: Wed, 6 Mar 2024 13:19:18 +0000
Subject: [PATCH 3/5] Migrate vscode testing to newer framework

---
 .vscode/settings.json.tmpl                    |  5 ++
 doc/HACKING.md                                |  7 +-
 integration/vscode/ada/.vscode-test.mjs       | 86 +++++++++++++++++++
 integration/vscode/ada/.vscodeignore          |  1 +
 integration/vscode/ada/package.json           |  6 +-
 .../ada/test/suite/general/codelens.test.ts   |  1 -
 .../ada/test/suite/general/debug.test.ts      |  1 -
 .../vscode/ada/test/suite/general/env.test.ts |  1 -
 .../ada/test/suite/general/extension.test.ts  |  1 -
 .../test/suite/general/highlighting.test.ts   |  1 -
 .../vscode/ada/test/suite/general/index.ts    |  5 --
 .../ada/test/suite/general/syntax.test.ts     |  1 -
 .../ada/test/suite/general/tasks.test.ts      |  1 -
 .../ada/test/suite/gnattest/gnattest.test.ts  |  1 -
 .../vscode/ada/test/suite/gnattest/index.ts   |  5 --
 integration/vscode/ada/test/suite/utils.ts    | 60 -------------
 .../suite/workspace_missing_dirs/index.ts     |  5 --
 .../workspace_missing_dirs.test.ts            |  1 -
 18 files changed, 98 insertions(+), 91 deletions(-)
 create mode 100644 integration/vscode/ada/.vscode-test.mjs
 delete mode 100644 integration/vscode/ada/test/suite/general/index.ts
 delete mode 100644 integration/vscode/ada/test/suite/gnattest/index.ts
 delete mode 100644 integration/vscode/ada/test/suite/workspace_missing_dirs/index.ts

diff --git a/.vscode/settings.json.tmpl b/.vscode/settings.json.tmpl
index 3b9325730..bec9ca153 100644
--- a/.vscode/settings.json.tmpl
+++ b/.vscode/settings.json.tmpl
@@ -88,5 +88,10 @@
       "**/node_modules/*/**": true,
       "**/.hg/store/**": true,
       ".obj/": true
+   },
+   "extension-test-runner.extractSettings": {
+      "suite": ["describe", "suite"],
+      "test": ["it", "test"],
+      "extractWith": "syntax"
    }
 }
diff --git a/doc/HACKING.md b/doc/HACKING.md
index 9c6dee45c..1c2df3881 100644
--- a/doc/HACKING.md
+++ b/doc/HACKING.md
@@ -142,10 +142,9 @@ To write a functional test for Ada Language Server:
 Run `make vscode-test` to run the VS Code testsuite.
 
 If you open the ALS repository in VS Code, it is also possible to run VS Code
-integration tests using the provided launch configurations:
-
-- `(vscode) Run testsuite 'general'`
-- `(vscode) Run testsuite 'gnattest'`
+integration tests using the Testing view.
+The `integration/vscode/ada/.vscode-test.mjs` contains a configuration allowing to load the tests in the Testing view.
+The UI offers ways to run all or a subset of the tests.
 
 ### Other tests
 
diff --git a/integration/vscode/ada/.vscode-test.mjs b/integration/vscode/ada/.vscode-test.mjs
new file mode 100644
index 000000000..44cb3a281
--- /dev/null
+++ b/integration/vscode/ada/.vscode-test.mjs
@@ -0,0 +1,86 @@
+import { defineConfig } from '@vscode/test-cli';
+import { mkdtempSync } from 'fs';
+import * as os from 'os';
+import { join } from 'path';
+
+let baseMochaOptions = {
+    ui: 'tdd',
+    color: true,
+};
+
+if (process.env.MOCHA_REPORTER) {
+    // If a reporter was specified externally, use it. For example, the CI
+    // environment could set this to 'mocha-junit-reporter' to produce JUnit
+    // results.
+    baseMochaOptions.reporter = process.env.MOCHA_REPORTER;
+}
+
+if (!baseMochaOptions.reporterOptions) {
+    baseMochaOptions.reporterOptions = {
+        maxDiffSize: 0,
+    };
+}
+
+if (process.env['MOCHA_TIMEOUT']) {
+    baseMochaOptions.timeout = process.env['MOCHA_TIMEOUT'];
+} else {
+    /**
+     * Some tests involve calling gprbuild which takes time. So we disable test
+     * timeouts altogether.
+     */
+    baseMochaOptions.timeout = '0';
+}
+
+if (process.env['MOCHA_GREP']) {
+    baseMochaOptions.grep = process.env['MOCHA_GREP'];
+}
+
+const testsuites = ['general', 'gnattest', 'workspace_missing_dirs'];
+
+export default defineConfig(
+    testsuites.map((suiteName) => {
+        // --user-data-dir is set to a unique dirctory under the OS
+        // default tmp directory for temporary files to avoid
+        // warnings related to longs paths in IPC sockets created by
+        // VSCode. The directory is made unique to avoid
+        // interference between successive runs.
+        //
+        // It also allows multiple testsuites to run concurrently with each VS
+        // Code instance using a different User data directory. This can happen
+        // when tests are launched from the VS Code UI.
+        const tmpdir = mkdtempSync(`${os.tmpdir()}/vsc-ada-test-`);
+
+        // Create a mocha options objects by copying the base one
+        let mochaOptions = { ...baseMochaOptions };
+
+        if (process.env.MOCHA_REPORTER) {
+            /**
+             * Produce results for each testsuite separately
+             */
+            const mochaFile = process.env.MOCHA_RESULTS_DIR
+                ? join(process.env.MOCHA_RESULTS_DIR, `${suiteName}.xml`)
+                : `${suiteName}.xml`;
+            mochaOptions.reporterOptions = { mochaFile: mochaFile };
+        }
+
+        return {
+            label: `Ada extension testsuite: ${suiteName}`,
+            files: `out/test/suite/${suiteName}/**/*.test.js`,
+            workspaceFolder: `./test/workspaces/${suiteName}`,
+            mocha: mochaOptions,
+            env: {
+                // When working remotely on Linux, it is necessary to have "Xvfb
+                // :99" running in the background, and this env variable set for
+                // the VS Code instances spawned for testing.
+                //
+                // This may prevent running locally on Linux and having the test
+                // windows visible, but we consider this a minor use case for
+                // now. A workaround is to remove this line.
+                DISPLAY: ':99',
+            },
+            launchArgs: ['--user-data-dir', tmpdir],
+            // Use external installation if provided in the VSCODE env variable
+            useInstallation: process.env.VSCODE ? { fromPath: process.env.VSCODE } : undefined,
+        };
+    })
+);
diff --git a/integration/vscode/ada/.vscodeignore b/integration/vscode/ada/.vscodeignore
index a195a29d3..9c0e0a8e4 100644
--- a/integration/vscode/ada/.vscodeignore
+++ b/integration/vscode/ada/.vscodeignore
@@ -1,4 +1,5 @@
 # This file contains patterns to exclude from the packaging into a .vsix
+.vscode-test.mjs
 .vscode-test/
 advanced/
 node_modules/
diff --git a/integration/vscode/ada/package.json b/integration/vscode/ada/package.json
index 5afb4d7c8..b0f30a6f1 100644
--- a/integration/vscode/ada/package.json
+++ b/integration/vscode/ada/package.json
@@ -883,9 +883,9 @@
         "pretest": "npm run compile",
         "lint": "eslint \"./src/**/*.{js,ts,tsx}\" --quiet --fix",
         "cilint": "eslint \"./src/**/*.{js,ts,tsx}\"",
-        "test": "node ./out/test/runTest.js",
-        "vscode-test": "vscode-test",
-        "resolve-backtrace": "npx stacktracify"
+        "test": "vscode-test",
+        "resolve-backtrace": "npx stacktracify",
+        "clean": "node -e \"fs.rmSync('out',{force:true,recursive:true})\""
     },
     "dependencies": {
         "@types/command-exists": "1.2.0",
diff --git a/integration/vscode/ada/test/suite/general/codelens.test.ts b/integration/vscode/ada/test/suite/general/codelens.test.ts
index eef1fceb5..c07ca4a03 100644
--- a/integration/vscode/ada/test/suite/general/codelens.test.ts
+++ b/integration/vscode/ada/test/suite/general/codelens.test.ts
@@ -1,5 +1,4 @@
 import assert from 'assert';
-import { suite, test } from 'mocha';
 import { Uri, window, workspace } from 'vscode';
 import { adaExtState } from '../../../src/extension';
 import { activate } from '../utils';
diff --git a/integration/vscode/ada/test/suite/general/debug.test.ts b/integration/vscode/ada/test/suite/general/debug.test.ts
index 33a515a59..92b146cb7 100644
--- a/integration/vscode/ada/test/suite/general/debug.test.ts
+++ b/integration/vscode/ada/test/suite/general/debug.test.ts
@@ -1,5 +1,4 @@
 import assert from 'assert';
-import { suite, test } from 'mocha';
 import {
     AdaConfig,
     adaDynamicDebugConfigProvider,
diff --git a/integration/vscode/ada/test/suite/general/env.test.ts b/integration/vscode/ada/test/suite/general/env.test.ts
index e2fda93ed..8e412d37b 100644
--- a/integration/vscode/ada/test/suite/general/env.test.ts
+++ b/integration/vscode/ada/test/suite/general/env.test.ts
@@ -1,5 +1,4 @@
 import assert from 'assert';
-import { suite, test } from 'mocha';
 import { setTerminalEnvironment } from '../../../src/helpers';
 
 suite('Environment init', () => {
diff --git a/integration/vscode/ada/test/suite/general/extension.test.ts b/integration/vscode/ada/test/suite/general/extension.test.ts
index e11f57486..268d93db1 100644
--- a/integration/vscode/ada/test/suite/general/extension.test.ts
+++ b/integration/vscode/ada/test/suite/general/extension.test.ts
@@ -1,6 +1,5 @@
 import * as assert from 'assert';
 import { adaExtState } from '../../../src/extension';
-import { suite, test } from 'mocha';
 import { getProjectFile, getObjectDir } from '../../../src/helpers';
 import { assertEqualToFileContent, activate } from '../utils';
 
diff --git a/integration/vscode/ada/test/suite/general/highlighting.test.ts b/integration/vscode/ada/test/suite/general/highlighting.test.ts
index d5184af8a..a4f70b74d 100644
--- a/integration/vscode/ada/test/suite/general/highlighting.test.ts
+++ b/integration/vscode/ada/test/suite/general/highlighting.test.ts
@@ -1,7 +1,6 @@
 import assert from 'assert';
 import * as vscode from 'vscode';
 import { spawnSync } from 'child_process';
-import { suite, test } from 'mocha';
 import { existsSync, opendirSync, renameSync } from 'fs';
 import path, { basename, dirname } from 'path';
 import { SemanticTokensParams, SemanticTokensRequest, integer } from 'vscode-languageclient';
diff --git a/integration/vscode/ada/test/suite/general/index.ts b/integration/vscode/ada/test/suite/general/index.ts
deleted file mode 100644
index fc214d3df..000000000
--- a/integration/vscode/ada/test/suite/general/index.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-import { runMochaTestsuite } from './../utils';
-
-export function run(): Promise<void> {
-    return runMochaTestsuite('general', __dirname);
-}
diff --git a/integration/vscode/ada/test/suite/general/syntax.test.ts b/integration/vscode/ada/test/suite/general/syntax.test.ts
index dd2f99a26..48e8836ac 100644
--- a/integration/vscode/ada/test/suite/general/syntax.test.ts
+++ b/integration/vscode/ada/test/suite/general/syntax.test.ts
@@ -1,7 +1,6 @@
 import * as assert from 'assert';
 import { adaExtState } from '../../../src/extension';
 import { AdaGrammarRule, AdaSyntaxCheckProvider } from '../../../src/alsProtocolExtensions';
-import { suite, test } from 'mocha';
 import { activate } from '../utils';
 
 suite('Syntax Check Test Suite', function () {
diff --git a/integration/vscode/ada/test/suite/general/tasks.test.ts b/integration/vscode/ada/test/suite/general/tasks.test.ts
index abe72580a..eb02c1976 100644
--- a/integration/vscode/ada/test/suite/general/tasks.test.ts
+++ b/integration/vscode/ada/test/suite/general/tasks.test.ts
@@ -1,6 +1,5 @@
 import assert from 'assert';
 import { existsSync } from 'fs';
-import { suite, test } from 'mocha';
 import * as vscode from 'vscode';
 import { exe, getProjectFile } from '../../../src/helpers';
 import {
diff --git a/integration/vscode/ada/test/suite/gnattest/gnattest.test.ts b/integration/vscode/ada/test/suite/gnattest/gnattest.test.ts
index b7c324ecd..3d395253f 100644
--- a/integration/vscode/ada/test/suite/gnattest/gnattest.test.ts
+++ b/integration/vscode/ada/test/suite/gnattest/gnattest.test.ts
@@ -1,4 +1,3 @@
-import { suite } from 'mocha';
 import { activate } from '../utils';
 
 suite('GNATtest Integration Tests', function () {
diff --git a/integration/vscode/ada/test/suite/gnattest/index.ts b/integration/vscode/ada/test/suite/gnattest/index.ts
deleted file mode 100644
index 672e2c15f..000000000
--- a/integration/vscode/ada/test/suite/gnattest/index.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-import { runMochaTestsuite } from './../utils';
-
-export function run(): Promise<void> {
-    return runMochaTestsuite('gnattest', __dirname);
-}
diff --git a/integration/vscode/ada/test/suite/utils.ts b/integration/vscode/ada/test/suite/utils.ts
index de59b69ed..f1938daa0 100644
--- a/integration/vscode/ada/test/suite/utils.ts
+++ b/integration/vscode/ada/test/suite/utils.ts
@@ -60,63 +60,3 @@ export async function activate(): Promise<void> {
         }
     }
 }
-
-export function runMochaTestsuite(suiteName: string, suiteDirectory: string) {
-    const mochaOptions: MochaOptions = {
-        ui: 'bdd',
-        color: true,
-    };
-
-    if (process.env.MOCHA_REPORTER) {
-        // If a reporter was specified externally, use it. For example, the CI
-        // environment could set this to 'mocha-junit-reporter' to produce JUnit
-        // results.
-        mochaOptions.reporter = process.env.MOCHA_REPORTER;
-        const mochaFile = process.env.MOCHA_RESULTS_DIR
-            ? path.join(`${process.env.MOCHA_RESULTS_DIR}`, `${suiteName}.xml`)
-            : `${suiteName}.xml`;
-        mochaOptions.reporterOptions = {
-            mochaFile,
-        };
-    }
-
-    if (!mochaOptions.reporterOptions) {
-        mochaOptions.reporterOptions = {
-            maxDiffSize: 0,
-        };
-    }
-
-    // Create the mocha test
-    const mocha = new Mocha(mochaOptions);
-
-    return new Promise<void>((resolve, reject) => {
-        const globOptions: GlobOptionsWithFileTypesUnset = { cwd: suiteDirectory };
-        const glob = new Glob('**/*.test.js', globOptions);
-        for (const file of glob) {
-            mocha.addFile(path.resolve(suiteDirectory, file));
-        }
-        try {
-            // This variable is set in the launch configuration (launch.json) of
-            // the VS Code workspace to allow debugging without triggering test
-            // timeouts.
-            if (env['MOCHA_TIMEOUT']) {
-                mocha.timeout(env['MOCHA_TIMEOUT']);
-            }
-
-            if (env['MOCHA_GREP']) {
-                mocha.grep(env['MOCHA_GREP']);
-            }
-
-            // Run the mocha test
-            mocha.run((failures) => {
-                if (failures > 0) {
-                    reject(new Error(`${failures} tests failed.`));
-                } else {
-                    resolve();
-                }
-            });
-        } catch (err) {
-            reject(err);
-        }
-    });
-}
diff --git a/integration/vscode/ada/test/suite/workspace_missing_dirs/index.ts b/integration/vscode/ada/test/suite/workspace_missing_dirs/index.ts
deleted file mode 100644
index bb9c85217..000000000
--- a/integration/vscode/ada/test/suite/workspace_missing_dirs/index.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-import { runMochaTestsuite } from '../utils';
-
-export function run(): Promise<void> {
-    return runMochaTestsuite('workspace_missing_dirs', __dirname);
-}
diff --git a/integration/vscode/ada/test/suite/workspace_missing_dirs/workspace_missing_dirs.test.ts b/integration/vscode/ada/test/suite/workspace_missing_dirs/workspace_missing_dirs.test.ts
index 619991ba8..d9a7afd10 100644
--- a/integration/vscode/ada/test/suite/workspace_missing_dirs/workspace_missing_dirs.test.ts
+++ b/integration/vscode/ada/test/suite/workspace_missing_dirs/workspace_missing_dirs.test.ts
@@ -1,5 +1,4 @@
 import * as assert from 'assert';
-import { suite, test } from 'mocha';
 import { activate } from '../utils';
 
 import * as vscode from 'vscode';

From 82bfe5a18dc2639dce69c61bd4c1ecb67c02b6a6 Mon Sep 17 00:00:00 2001
From: Anthony Leonardo Gracio <leonardo@adacore.com>
Date: Fri, 22 Mar 2024 10:45:05 +0000
Subject: [PATCH 4/5] Apply 1 suggestion(s) to 1 file(s)

---
 integration/vscode/ada/.vscode-test.mjs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/integration/vscode/ada/.vscode-test.mjs b/integration/vscode/ada/.vscode-test.mjs
index 44cb3a281..3a5d36d5a 100644
--- a/integration/vscode/ada/.vscode-test.mjs
+++ b/integration/vscode/ada/.vscode-test.mjs
@@ -39,7 +39,7 @@ const testsuites = ['general', 'gnattest', 'workspace_missing_dirs'];
 
 export default defineConfig(
     testsuites.map((suiteName) => {
-        // --user-data-dir is set to a unique dirctory under the OS
+        // --user-data-dir is set to a unique directory under the OS
         // default tmp directory for temporary files to avoid
         // warnings related to longs paths in IPC sockets created by
         // VSCode. The directory is made unique to avoid

From 772eb16925bc8fe58dc48ac8d3b77a96bc7c2233 Mon Sep 17 00:00:00 2001
From: Elie Richa <richa@adacore.com>
Date: Fri, 22 Mar 2024 11:43:39 +0000
Subject: [PATCH 5/5] Add Extension Test Runner to recommended extensions

---
 .vscode/extensions.json | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/.vscode/extensions.json b/.vscode/extensions.json
index 6fdf84f32..1736b5d26 100644
--- a/.vscode/extensions.json
+++ b/.vscode/extensions.json
@@ -8,6 +8,7 @@
       "esbenp.prettier-vscode",
       "gruntfuggly.triggertaskonsave",
       "davidanson.vscode-markdownlint",
-      "adacore.ada"
+      "adacore.ada",
+      "ms-vscode.extension-test-runner"
    ]
 }