diff --git a/src/Analysis/Ast/Impl/Documents/Definitions/IRunningDocumentTable.cs b/src/Analysis/Ast/Impl/Documents/Definitions/IRunningDocumentTable.cs
index 0debc17ad..442327bd8 100644
--- a/src/Analysis/Ast/Impl/Documents/Definitions/IRunningDocumentTable.cs
+++ b/src/Analysis/Ast/Impl/Documents/Definitions/IRunningDocumentTable.cs
@@ -70,6 +70,12 @@ public interface IRunningDocumentTable {
/// New lock count or -1 if document was not found.
int UnlockDocument(Uri uri);
+ ///
+ /// Reloads the table by removing all unopened files (which would have been loaded from disk),
+ /// and resetting the content of all other files to trigger reanalysis.
+ ///
+ void ReloadAll();
+
///
/// Fires when document is opened.
///
diff --git a/src/Analysis/Ast/Impl/Documents/RunningDocumentTable.cs b/src/Analysis/Ast/Impl/Documents/RunningDocumentTable.cs
index a88533762..cb99f182d 100644
--- a/src/Analysis/Ast/Impl/Documents/RunningDocumentTable.cs
+++ b/src/Analysis/Ast/Impl/Documents/RunningDocumentTable.cs
@@ -21,6 +21,7 @@
using Microsoft.Python.Analysis.Analyzer;
using Microsoft.Python.Analysis.Modules;
using Microsoft.Python.Core;
+using Microsoft.Python.Core.Collections;
using Microsoft.Python.Core.Logging;
namespace Microsoft.Python.Analysis.Documents {
@@ -189,6 +190,29 @@ public void CloseDocument(Uri documentUri) {
}
}
+ public void ReloadAll() {
+ ImmutableArray> opened;
+ ImmutableArray> closed;
+
+ lock (_lock) {
+ _documentsByUri.Split(kvp => kvp.Value.Document.IsOpen, out opened, out closed);
+
+ foreach (var (uri, entry) in closed) {
+ _documentsByUri.Remove(uri);
+ entry.Document.Dispose();
+ }
+ }
+
+ foreach (var (_, entry) in closed) {
+ Closed?.Invoke(this, new DocumentEventArgs(entry.Document));
+ Removed?.Invoke(this, new DocumentEventArgs(entry.Document));
+ }
+
+ foreach (var (_, entry) in opened) {
+ entry.Document.Reset(null);
+ }
+ }
+
public void Dispose() {
lock (_lock) {
foreach (var d in _documentsByUri.Values.OfType()) {
diff --git a/src/Analysis/Ast/Impl/Modules/Resolution/MainModuleResolution.cs b/src/Analysis/Ast/Impl/Modules/Resolution/MainModuleResolution.cs
index cccb576c6..2ebee0d95 100644
--- a/src/Analysis/Ast/Impl/Modules/Resolution/MainModuleResolution.cs
+++ b/src/Analysis/Ast/Impl/Modules/Resolution/MainModuleResolution.cs
@@ -38,6 +38,8 @@ internal sealed class MainModuleResolution : ModuleResolutionBase, IModuleManage
private readonly ConcurrentDictionary _specialized = new ConcurrentDictionary();
private IRunningDocumentTable _rdt;
+ private IEnumerable _userPaths = Enumerable.Empty();
+
public MainModuleResolution(string root, IServiceContainer services)
: base(root, services) { }
@@ -75,7 +77,7 @@ protected override IPythonModule CreateModule(string name) {
return module;
}
}
-
+
// If there is a stub, make sure it is loaded and attached
// First check stub next to the module.
if (!TryCreateModuleStub(name, moduleImport.ModulePath, out var stub)) {
@@ -169,6 +171,26 @@ internal async Task LoadBuiltinTypesAsync(CancellationToken cancellationToken =
}
}
+ internal async Task ReloadSearchPaths(CancellationToken cancellationToken = default) {
+ var ps = _services.GetService();
+
+ var paths = await GetInterpreterSearchPathsAsync(cancellationToken);
+ var (interpreterPaths, userPaths) = PythonLibraryPath.ClassifyPaths(Root, _fs, paths, Configuration.SearchPaths);
+
+ InterpreterPaths = interpreterPaths.Select(p => p.Path);
+ _userPaths = userPaths.Select(p => p.Path);
+
+ _log?.Log(TraceEventType.Information, "Interpreter search paths:");
+ foreach (var s in InterpreterPaths) {
+ _log?.Log(TraceEventType.Information, $" {s}");
+ }
+
+ _log?.Log(TraceEventType.Information, "User search paths:");
+ foreach (var s in _userPaths) {
+ _log?.Log(TraceEventType.Information, $" {s}");
+ }
+ }
+
public async Task ReloadAsync(CancellationToken cancellationToken = default) {
foreach (var uri in Modules
.Where(m => m.Value.Value?.Name != BuiltinModuleName)
@@ -176,7 +198,7 @@ public async Task ReloadAsync(CancellationToken cancellationToken = default) {
.ExcludeDefault()) {
GetRdt()?.UnlockDocument(uri);
}
-
+
// Preserve builtins, they don't need to be reloaded since interpreter does not change.
var builtins = Modules[BuiltinModuleName];
Modules.Clear();
@@ -187,26 +209,10 @@ public async Task ReloadAsync(CancellationToken cancellationToken = default) {
var addedRoots = new HashSet();
addedRoots.UnionWith(PathResolver.SetRoot(Root));
- var ps = _services.GetService();
-
- var paths = await GetInterpreterSearchPathsAsync(cancellationToken);
- var (interpreterPaths, userPaths) = PythonLibraryPath.ClassifyPaths(Root, _fs, paths, Configuration.SearchPaths);
-
- InterpreterPaths = interpreterPaths.Select(p => p.Path);
- var userSearchPaths = userPaths.Select(p => p.Path);
-
- _log?.Log(TraceEventType.Information, "Interpreter search paths:");
- foreach (var s in InterpreterPaths) {
- _log?.Log(TraceEventType.Information, $" {s}");
- }
-
- _log?.Log(TraceEventType.Information, "User search paths:");
- foreach (var s in userSearchPaths) {
- _log?.Log(TraceEventType.Information, $" {s}");
- }
+ await ReloadSearchPaths(cancellationToken);
addedRoots.UnionWith(PathResolver.SetInterpreterSearchPaths(InterpreterPaths));
- addedRoots.UnionWith(PathResolver.SetUserSearchPaths(userSearchPaths));
+ addedRoots.UnionWith(PathResolver.SetUserSearchPaths(_userPaths));
ReloadModulePaths(addedRoots);
}
diff --git a/src/LanguageServer/Impl/Implementation/Server.cs b/src/LanguageServer/Impl/Implementation/Server.cs
index 7bb4571a6..799924384 100644
--- a/src/LanguageServer/Impl/Implementation/Server.cs
+++ b/src/LanguageServer/Impl/Implementation/Server.cs
@@ -49,6 +49,10 @@ public sealed partial class Server : IDisposable {
private IIndexManager _indexManager;
private string _rootDir;
+ private bool _watchSearchPaths;
+ private PathsWatcher _pathsWatcher;
+ private string[] _searchPaths;
+
public Server(IServiceManager services) {
_services = services;
@@ -58,6 +62,7 @@ public Server(IServiceManager services) {
ext.Dispose();
}
})
+ .Add(() => _pathsWatcher?.Dispose())
.Add(() => _shutdownCts.Cancel());
}
@@ -173,11 +178,11 @@ public void DidChangeConfiguration(DidChangeConfigurationParams @params, Cancell
_disposableBag.ThrowIfDisposed();
switch (@params.settings) {
case ServerSettings settings: {
- if (HandleConfigurationChanges(settings)) {
- RestartAnalysis();
+ if (HandleConfigurationChanges(settings)) {
+ RestartAnalysis();
+ }
+ break;
}
- break;
- }
default:
_log?.Log(TraceEventType.Error, "change configuration notification sent unsupported settings");
break;
@@ -233,7 +238,34 @@ private IDocumentationSource ChooseDocumentationSource(string[] kinds) {
}
#endregion
- public void NotifyPackagesChanged(CancellationToken cancellationToken) {
+ public void HandleWatchPathsChange(bool watchSearchPaths) {
+ if (watchSearchPaths == _watchSearchPaths) {
+ return;
+ }
+
+ _watchSearchPaths = watchSearchPaths;
+
+ if (!_watchSearchPaths) {
+ _searchPaths = null;
+ _pathsWatcher?.Dispose();
+ _pathsWatcher = null;
+ return;
+ }
+
+ ResetPathWatcher();
+ }
+
+ private void ResetPathWatcher() {
+ var paths = _interpreter.ModuleResolution.InterpreterPaths.ToArray();
+
+ if (_searchPaths == null || !_searchPaths.SequenceEqual(paths)) {
+ _searchPaths = paths;
+ _pathsWatcher?.Dispose();
+ _pathsWatcher = new PathsWatcher(_searchPaths, () => NotifyPackagesChanged(), _log);
+ }
+ }
+
+ public void NotifyPackagesChanged(CancellationToken cancellationToken = default) {
var interpreter = _services.GetService();
_log?.Log(TraceEventType.Information, Resources.ReloadingModules);
// No need to reload typeshed resolution since it is a static storage.
@@ -242,17 +274,20 @@ public void NotifyPackagesChanged(CancellationToken cancellationToken) {
interpreter.ModuleResolution.ReloadAsync(cancellationToken).ContinueWith(t => {
_log?.Log(TraceEventType.Information, Resources.Done);
_log?.Log(TraceEventType.Information, Resources.AnalysisRestarted);
+
RestartAnalysis();
+
+ if (_watchSearchPaths) {
+ ResetPathWatcher();
+ }
}, cancellationToken).DoNotWait();
}
private void RestartAnalysis() {
- var analyzer = Services.GetService();;
+ var analyzer = Services.GetService();
analyzer.ResetAnalyzer();
- foreach (var doc in _rdt.GetDocuments()) {
- doc.Reset(null);
- }
+ _rdt.ReloadAll();
}
}
}
diff --git a/src/LanguageServer/Impl/LanguageServer.Configuration.cs b/src/LanguageServer/Impl/LanguageServer.Configuration.cs
index 180a8fb21..1129c6a83 100644
--- a/src/LanguageServer/Impl/LanguageServer.Configuration.cs
+++ b/src/LanguageServer/Impl/LanguageServer.Configuration.cs
@@ -51,7 +51,7 @@ public async Task DidChangeConfiguration(JToken token, CancellationToken cancell
settings.symbolsHierarchyMaxSymbols = GetSetting(analysis, "symbolsHierarchyMaxSymbols", 1000);
_logger.LogLevel = GetLogLevel(analysis).ToTraceEventType();
- HandlePathWatchChanges(token, cancellationToken);
+ HandlePathWatchChanges(token);
HandleDiagnosticsChanges(pythonSection, settings);
_server.DidChangeConfiguration(new DidChangeConfigurationParams { settings = settings }, cancellationToken);
diff --git a/src/LanguageServer/Impl/LanguageServer.cs b/src/LanguageServer/Impl/LanguageServer.cs
index f61e951ce..04f48b9fe 100644
--- a/src/LanguageServer/Impl/LanguageServer.cs
+++ b/src/LanguageServer/Impl/LanguageServer.cs
@@ -55,12 +55,8 @@ public sealed partial class LanguageServer : IDisposable {
private JsonRpc _rpc;
private JsonSerializer _jsonSerializer;
- private PathsWatcher _pathsWatcher;
private IIdleTimeTracker _idleTimeTracker;
- private bool _watchSearchPaths;
- private string[] _searchPaths = Array.Empty();
-
public CancellationToken Start(IServiceManager services, JsonRpc rpc) {
_server = new Server(services);
_services = services;
@@ -78,7 +74,6 @@ public CancellationToken Start(IServiceManager services, JsonRpc rpc) {
_disposables
.Add(() => _shutdownCts.Cancel())
.Add(_prioritizer)
- .Add(() => _pathsWatcher?.Dispose())
.Add(() => _rpc.TraceSource.Listeners.Remove(rpcTraceListener));
services.AddService(_optionsProvider);
@@ -357,30 +352,8 @@ private MessageType GetLogLevel(JToken analysisKey) {
return MessageType.Error;
}
- private void HandlePathWatchChanges(JToken section, CancellationToken cancellationToken) {
- var watchSearchPaths = GetSetting(section, "watchSearchPaths", true);
- if (!watchSearchPaths) {
- // No longer watching.
- _pathsWatcher?.Dispose();
- _searchPaths = Array.Empty();
- _watchSearchPaths = false;
- return;
- }
-
- // Now watching.
- if (!_watchSearchPaths || (_watchSearchPaths && _searchPaths.SetEquals(_initParams.initializationOptions.searchPaths))) {
- // Were not watching OR were watching but paths have changed. Recreate the watcher.
- _pathsWatcher?.Dispose();
- _pathsWatcher = new PathsWatcher(
- _initParams.initializationOptions.searchPaths,
- () =>_server.NotifyPackagesChanged(cancellationToken),
- _services.GetService()
- );
-
- _watchSearchPaths = true;
- _searchPaths = _initParams.initializationOptions.searchPaths;
- }
- }
+ private void HandlePathWatchChanges(JToken section)
+ => _server.HandleWatchPathsChange(GetSetting(section, "watchSearchPaths", true));
private static CancellationToken GetToken(CancellationToken original)
=> Debugger.IsAttached ? CancellationToken.None : original;
diff --git a/src/LanguageServer/Impl/PathsWatcher.cs b/src/LanguageServer/Impl/PathsWatcher.cs
index fe331385d..f92b7fd8e 100644
--- a/src/LanguageServer/Impl/PathsWatcher.cs
+++ b/src/LanguageServer/Impl/PathsWatcher.cs
@@ -43,6 +43,7 @@ public PathsWatcher(string[] paths, Action onChanged, ILogger log) {
_onChanged = onChanged;
var reduced = ReduceToCommonRoots(paths);
+
foreach (var p in reduced) {
try {
if (!Directory.Exists(p)) {
@@ -53,20 +54,29 @@ public PathsWatcher(string[] paths, Action onChanged, ILogger log) {
continue;
}
+ _log.Log(TraceEventType.Verbose, $"Watching {p}");
+
try {
var fsw = new System.IO.FileSystemWatcher(p) {
IncludeSubdirectories = true,
EnableRaisingEvents = true,
- NotifyFilter = NotifyFilters.FileName | NotifyFilters.DirectoryName
+ NotifyFilter = NotifyFilters.FileName | NotifyFilters.DirectoryName | NotifyFilters.LastWrite,
+ InternalBufferSize = 1 << 16, // Max buffer size of 64 KB
};
+ fsw.Changed += OnChanged;
fsw.Created += OnChanged;
fsw.Deleted += OnChanged;
+ fsw.Renamed += OnChanged;
+
+ fsw.Filter = "*.p*"; // .py, .pyc, .pth - TODO: Use Filters in .NET Core 3.0.
_disposableBag
.Add(() => _throttleTimer?.Dispose())
+ .Add(() => fsw.Changed -= OnChanged)
.Add(() => fsw.Created -= OnChanged)
.Add(() => fsw.Deleted -= OnChanged)
+ .Add(() => fsw.Renamed -= OnChanged)
.Add(() => fsw.EnableRaisingEvents = false)
.Add(fsw);
} catch (ArgumentException ex) {