public async Task <Unit> Handle(DidChangeTextDocumentParams notification, CancellationToken token) { var uri = notification.TextDocument.Uri.GetAbsoluteOrUNCPath(); var document = await _projectSnapshotManagerDispatcher.RunOnDispatcherThreadAsync(() => { _documentResolver.TryResolveDocument(uri, out var documentSnapshot); return(documentSnapshot); }, CancellationToken.None).ConfigureAwait(false); if (document is null) { throw new InvalidOperationException(RazorLS.Resources.FormatDocument_Not_Found(uri)); } var sourceText = await document.GetTextAsync(); sourceText = ApplyContentChanges(notification.ContentChanges, sourceText); if (notification.TextDocument.Version is null) { throw new InvalidOperationException(RazorLS.Resources.Version_Should_Not_Be_Null); } await _projectSnapshotManagerDispatcher.RunOnDispatcherThreadAsync( () => _projectService.UpdateDocument(document.FilePath, sourceText, notification.TextDocument.Version.Value), CancellationToken.None).ConfigureAwait(false); return(Unit.Value); }
public async Task StartAsync(string workspaceDirectory, CancellationToken cancellationToken) { if (workspaceDirectory is null) { throw new ArgumentNullException(nameof(workspaceDirectory)); } // Dive through existing project files and fabricate "added" events so listeners can accurately listen to state changes for them. workspaceDirectory = _filePathNormalizer.Normalize(workspaceDirectory); var existingProjectFiles = GetExistingProjectFiles(workspaceDirectory); await _projectSnapshotManagerDispatcher.RunOnDispatcherThreadAsync(() => { foreach (var projectFilePath in existingProjectFiles) { FileSystemWatcher_ProjectFileEvent(projectFilePath, RazorFileChangeKind.Added); } }, cancellationToken).ConfigureAwait(false); // This is an entry point for testing OnInitializationFinished(); if (cancellationToken.IsCancellationRequested) { // Client cancelled connection, no need to setup any file watchers. Server is about to tear down. return; } // Start listening for project file changes (added/removed/renamed). _watcher = new RazorFileSystemWatcher(workspaceDirectory, ProjectFileExtensionPattern) { NotifyFilter = NotifyFilters.FileName | NotifyFilters.LastWrite | NotifyFilters.CreationTime, IncludeSubdirectories = true, }; _watcher.Created += (sender, args) => FileSystemWatcher_ProjectFileEvent_Background(args.FullPath, RazorFileChangeKind.Added); _watcher.Deleted += (sender, args) => FileSystemWatcher_ProjectFileEvent_Background(args.FullPath, RazorFileChangeKind.Removed); _watcher.Renamed += (sender, args) => { // Translate file renames into remove->add if (args.OldFullPath.EndsWith(ProjectFileExtension, FilePathComparison.Instance)) { // Renaming from project file to something else. FileSystemWatcher_ProjectFileEvent_Background(args.OldFullPath, RazorFileChangeKind.Removed); } if (args.FullPath.EndsWith(ProjectFileExtension, FilePathComparison.Instance)) { // Renaming to a project file. FileSystemWatcher_ProjectFileEvent_Background(args.FullPath, RazorFileChangeKind.Added); } }; _watcher.EnableRaisingEvents = true; }
public async override Task <TagHelperDescriptor?> TryGetTagHelperDescriptorAsync(DocumentSnapshot documentSnapshot, CancellationToken cancellationToken) { // No point doing anything if its not a component if (documentSnapshot.FileKind != FileKinds.Component) { return(null); } var razorCodeDocument = await documentSnapshot.GetGeneratedOutputAsync().ConfigureAwait(false); if (razorCodeDocument is null) { return(null); } var projects = await _projectSnapshotManagerDispatcher.RunOnDispatcherThreadAsync( () => _projectSnapshotManager.Projects.ToArray(), cancellationToken).ConfigureAwait(false); foreach (var project in projects) { // If the document is an import document, then it can't be a component if (project.IsImportDocument(documentSnapshot)) { return(null); } // If the document isn't in this project, then no point searching for components // This also avoids the issue of duplicate components if (!project.DocumentFilePaths.Contains(documentSnapshot.FilePath)) { return(null); } // If we got this far, we can check for tag helpers foreach (var tagHelper in project.TagHelpers) { // Check the typename and namespace match if (IsPathCandidateForComponent(documentSnapshot, tagHelper.GetTypeNameIdentifier()) && ComponentNamespaceMatchesFullyQualifiedName(razorCodeDocument, tagHelper.GetTypeNamespace())) { return(tagHelper); } } } return(null); }
public async Task SolutionClosing_CancelsActiveWork() { // Arrange var projectManager = new TestProjectSnapshotManager(Workspace) { AllowNotifyListeners = true, }; var expectedProjectPath = SomeProject.FilePath; var expectedProjectSnapshot = await Dispatcher.RunOnDispatcherThreadAsync(() => { projectManager.ProjectAdded(SomeProject); projectManager.ProjectAdded(SomeOtherProject); return(projectManager.GetLoadedProject(SomeProject.FilePath)); }, CancellationToken.None); var projectService = new Mock <TextBufferProjectService>(MockBehavior.Strict); projectService.Setup(p => p.GetProjectPath(It.IsAny <IVsHierarchy>())).Returns(expectedProjectPath); var workspaceStateGenerator = new TestProjectWorkspaceStateGenerator(); var trigger = new VsSolutionUpdatesProjectSnapshotChangeTrigger(TestServiceProvider.Instance, projectService.Object, workspaceStateGenerator, Dispatcher, JoinableTaskFactory.Context); trigger.Initialize(projectManager); trigger.UpdateProjectCfg_Done(Mock.Of <IVsHierarchy>(MockBehavior.Strict), pCfgProj: default, pCfgSln: default, dwAction: default, fSuccess: default, fCancel: default);
private void HandleFileChangeEvent(FileChangeKind changeKind, FileEventArgs args) { if (Changed == null) { return; } foreach (var fileEvent in args) { if (fileEvent.IsDirectory) { continue; } var normalizedEventPath = NormalizePath(fileEvent.FileName.FullPath); if (string.Equals(_normalizedFilePath, normalizedEventPath, StringComparison.OrdinalIgnoreCase)) { _ = _projectSnapshotManagerDispatcher.RunOnDispatcherThreadAsync((changeKind, ct) => { OnChanged(changeKind); }, changeKind, CancellationToken.None); return; } } }
private void UpdateDocuments(HostProject hostProject, IEnumerable <IMSBuildItemEvaluated> projectItems) { var projectDirectory = Path.GetDirectoryName(hostProject.FilePath); var documentFilePaths = GetRazorDocuments(projectDirectory, projectItems); var oldFiles = _currentRazorFilePaths; var newFiles = documentFilePaths.ToImmutableHashSet(); var addedFiles = newFiles.Except(oldFiles); var removedFiles = oldFiles.Except(newFiles); _currentRazorFilePaths = documentFilePaths; _ = ProjectSnapshotManagerDispatcher.RunOnDispatcherThreadAsync(() => { foreach (var document in removedFiles) { RemoveDocument(hostProject, document); } foreach (var document in addedFiles) { var relativeFilePath = document.Substring(projectDirectory.Length + 1); AddDocument(hostProject, document, relativeFilePath); } }, CancellationToken.None); }
private async Task InitializeSolutionAsync(Solution solution, CancellationToken cancellationToken) { try { await _projectSnapshotManagerDispatcher.RunOnDispatcherThreadAsync( () => { base.InitializeSolution(solution); }, cancellationToken).ConfigureAwait(false); } catch (Exception ex) { Debug.Fail("Unexpected error when initializing solution: " + ex); } }
private void Project_Disposing(object sender, EventArgs e) { _project.ProjectCapabilitiesChanged -= Project_ProjectCapabilitiesChanged; _project.Disposing -= Project_Disposing; _ = _projectSnapshotManagerDispatcher.RunOnDispatcherThreadAsync(() => DetachCurrentRazorProjectHost(), CancellationToken.None); }
internal Task HandleEndBuildAsync(BuildEventArgs args) { if (!args.Success) { // Build failed return(Task.CompletedTask); } return(_projectSnapshotManagerDispatcher.RunOnDispatcherThreadAsync((projectItem, ct) => { if (!_projectService.IsSupportedProject(projectItem)) { // We're hooked into all build events, it's possible to get called with an unsupported project item type. return; } var projectPath = _projectService.GetProjectPath(projectItem); var projectSnapshot = _projectManager.GetLoadedProject(projectPath); if (projectSnapshot != null) { var workspaceProject = _projectManager.Workspace.CurrentSolution?.Projects.FirstOrDefault( project => FilePathComparer.Instance.Equals(project.FilePath, projectSnapshot.FilePath)); if (workspaceProject != null) { // Trigger a tag helper update by forcing the project manager to see the workspace Project // from the current solution. _workspaceStateGenerator.Update(workspaceProject, projectSnapshot, CancellationToken.None); } } }, args.SolutionItem, CancellationToken.None)); }
public void ProcessOpen(ITextBuffer textBuffer) { if (textBuffer == null) { throw new ArgumentNullException(nameof(textBuffer)); } _ = _projectSnapshotManagerDispatcher.RunOnDispatcherThreadAsync( () => _fileTracker.StopListening(), CancellationToken.None).ConfigureAwait(false); _snapshotTracker.StartTracking(textBuffer); EditorTextBuffer = textBuffer; EditorTextContainer = textBuffer.AsTextContainer(); EditorTextContainer.TextChanged += TextContainer_Changed; _opened?.Invoke(this, EventArgs.Empty); }
public override ValueTask ProcessAsync(CancellationToken cancellationToken) { var task = _projectSnapshotManagerDispatcher.RunOnDispatcherThreadAsync( () => _workspaceStateGenerator.Update(_workspaceProject, _projectSnapshot, cancellationToken), cancellationToken); return(new ValueTask(task)); }
// We override the InitializeSolution in order to enforce calls to this to be on the project snapshot manager's // thread. OmniSharp currently has an issue where they update the Solution on multiple different threads resulting // in change events dispatching through the Workspace on multiple different threads. This normalizes // that abnormality. #pragma warning disable VSTHRD100 // Avoid async void methods protected override async void InitializeSolution(Solution solution) #pragma warning restore VSTHRD100 // Avoid async void methods { await _projectSnapshotManagerDispatcher.RunOnDispatcherThreadAsync( () => { try { base.InitializeSolution(solution); } catch (Exception ex) { Debug.Fail("Unexpected error when initializing solution: " + ex); } }, CancellationToken.None).ConfigureAwait(false); }
private async Task Document_ChangedOnDiskAsync(EditorDocument document, CancellationToken cancellationToken) { try { // This event is called by the EditorDocumentManager, which runs on the UI thread. // However, due to accessing the project snapshot manager, we need to switch to // running on the project snapshot manager's specialized thread. await _projectSnapshotManagerDispatcher.RunOnDispatcherThreadAsync(() => { ProjectManager.DocumentChanged(document.ProjectFilePath, document.DocumentFilePath, document.TextLoader); }, cancellationToken).ConfigureAwait(false); } catch (Exception ex) { Debug.Fail("EditorDocumentManagerListener.Document_ChangedOnDisk threw exception:" + Environment.NewLine + ex.Message + Environment.NewLine + "Stack trace:" + Environment.NewLine + ex.StackTrace); } }
#pragma warning disable VSTHRD100 // Avoid async void methods private async void Document_ChangedOnDisk(object sender, EventArgs e) #pragma warning restore VSTHRD100 // Avoid async void methods { try { // This event is called by the EditorDocumentManager, which runs on the UI thread. // However, due to accessing the project snapshot manager, we need to switch to // running on the project snapshot manager's specialized thread. await _projectSnapshotManagerDispatcher.RunOnDispatcherThreadAsync(() => { var document = (EditorDocument)sender; _projectManager.DocumentChanged(document.ProjectFilePath, document.DocumentFilePath, document.TextLoader); }, CancellationToken.None).ConfigureAwait(false); } catch (Exception ex) { Debug.Fail("EditorDocumentManagerListener.Document_ChangedOnDisk threw exception:" + Environment.NewLine + ex.Message + Environment.NewLine + "Stack trace:" + Environment.NewLine + ex.StackTrace); } }
/// <summary>Search for a component in a project based on its tag name and fully qualified name.</summary> /// <remarks> /// This method makes several assumptions about the nature of components. First, it assumes that a component /// a given name `Name` will be located in a file `Name.razor`. Second, it assumes that the namespace the /// component is present in has the same name as the assembly its corresponding tag helper is loaded from. /// Implicitly, this method inherits any assumptions made by TrySplitNamespaceAndType. /// </remarks> /// <param name="tagHelper">A TagHelperDescriptor to find the corresponding Razor component for.</param> /// <returns>The corresponding DocumentSnapshot if found, null otherwise.</returns> /// <exception cref="ArgumentNullException">Thrown if <paramref name="tagHelper"/> is null.</exception> public override async Task <DocumentSnapshot> TryLocateComponentAsync(TagHelperDescriptor tagHelper) { if (tagHelper is null) { throw new ArgumentNullException(nameof(tagHelper)); } if (!DefaultRazorTagHelperBinderPhase.ComponentDirectiveVisitor.TrySplitNamespaceAndType(tagHelper.Name, out var @namespaceName, out var typeName)) { _logger.LogWarning($"Could not split namespace and type for name {tagHelper.Name}."); return(null); } var lookupSymbolName = RemoveGenericContent(typeName); var projects = await _projectSnapshotManagerDispatcher.RunOnDispatcherThreadAsync( () => _projectSnapshotManager.Projects.ToArray(), CancellationToken.None).ConfigureAwait(false); foreach (var project in projects) { foreach (var path in project.DocumentFilePaths) { // Get document and code document var documentSnapshot = project.GetDocument(path); // Rule out if not Razor component with correct name if (!IsPathCandidateForComponent(documentSnapshot, lookupSymbolName)) { continue; } var razorCodeDocument = await documentSnapshot.GetGeneratedOutputAsync().ConfigureAwait(false); if (razorCodeDocument is null) { continue; } // Make sure we have the right namespace of the fully qualified name if (!ComponentNamespaceMatchesFullyQualifiedName(razorCodeDocument, namespaceName)) { continue; } return(documentSnapshot); } } return(null); }
public async override Task OnTextViewOpenedAsync(ITextView textView, IEnumerable <ITextBuffer> subjectBuffers) { if (textView is null) { throw new ArgumentNullException(nameof(textView)); } if (subjectBuffers is null) { throw new ArgumentNullException(nameof(subjectBuffers)); } _joinableTaskContext.AssertUIThread(); foreach (var textBuffer in subjectBuffers) { if (!textBuffer.IsRazorBuffer()) { continue; } if (!_editorFactoryService.TryGetDocumentTracker(textBuffer, out var documentTracker) || !(documentTracker is DefaultVisualStudioDocumentTracker tracker)) { Debug.Fail("Tracker should always be available given our expectations of the VS workflow."); return; } tracker.AddTextView(textView); if (documentTracker.TextViews.Count == 1) { // tracker.Subscribe() accesses the project snapshot manager, which needs to be run on the // project snapshot manager's specialized thread. await _projectSnapshotManagerDispatcher.RunOnDispatcherThreadAsync(() => tracker.Subscribe(), CancellationToken.None).ConfigureAwait(false); } } }
public DefaultProjectSnapshotManager( ProjectSnapshotManagerDispatcher projectSnapshotManagerDispatcher, ErrorReporter errorReporter, IEnumerable <ProjectSnapshotChangeTrigger> triggers, Workspace workspace) { if (projectSnapshotManagerDispatcher is null) { throw new ArgumentNullException(nameof(projectSnapshotManagerDispatcher)); } if (errorReporter is null) { throw new ArgumentNullException(nameof(errorReporter)); } if (triggers is null) { throw new ArgumentNullException(nameof(triggers)); } if (workspace is null) { throw new ArgumentNullException(nameof(workspace)); } _projectSnapshotManagerDispatcher = projectSnapshotManagerDispatcher; _errorReporter = errorReporter; _triggers = triggers.OrderByDescending(trigger => trigger.InitializePriority).ToArray(); Workspace = workspace; _projects = new Dictionary <string, Entry>(FilePathComparer.Instance); _openDocuments = new HashSet <string>(FilePathComparer.Instance); _notificationWork = new Queue <ProjectChangeEventArgs>(); // All methods involving the project snapshot manager need to be run on the // project snapshot manager's specialized thread. The LSP editor should already // be on the specialized thread, however the old editor may be calling this // constructor on the UI thread. if (_projectSnapshotManagerDispatcher.IsDispatcherThread) { InitializeTriggers(this, _triggers); } else { _ = _projectSnapshotManagerDispatcher.RunOnDispatcherThreadAsync( () => InitializeTriggers(this, _triggers), CancellationToken.None); }
internal Task OnProjectBuiltAsync(IVsHierarchy projectHierarchy, CancellationToken cancellationToken) { var projectFilePath = _projectService.GetProjectPath(projectHierarchy); return(_projectSnapshotManagerDispatcher.RunOnDispatcherThreadAsync(() => { var projectSnapshot = _projectManager.GetLoadedProject(projectFilePath); if (projectSnapshot != null) { var workspaceProject = _projectManager.Workspace.CurrentSolution.Projects.FirstOrDefault( wp => FilePathComparer.Instance.Equals(wp.FilePath, projectSnapshot.FilePath)); if (workspaceProject != null) { // Trigger a tag helper update by forcing the project manager to see the workspace Project // from the current solution. _workspaceStateGenerator.Update(workspaceProject, projectSnapshot, cancellationToken); } } }, cancellationToken)); }
#pragma warning disable VSTHRD100 // Avoid async void methods private async void Timer_Tick(object state) #pragma warning restore VSTHRD100 // Avoid async void methods { try { OnStartingBackgroundWork(); KeyValuePair <string, DocumentSnapshot>[] work; lock (_work) { work = _work.ToArray(); _work.Clear(); } OnBackgroundCapturedWorkload(); for (var i = 0; i < work.Length; i++) { if (_solutionIsClosing) { break; } var document = work[i].Value; try { await document.GetGeneratedOutputAsync().ConfigureAwait(false); } catch (Exception ex) { ReportError(ex); } } OnCompletingBackgroundWork(); if (_documentProcessedListeners.Count != 0 && !_solutionIsClosing) { await _projectSnapshotManagerDispatcher.RunOnDispatcherThreadAsync( () => NotifyDocumentsProcessed(work), CancellationToken.None).ConfigureAwait(false); } lock (_work) { // Resetting the timer allows another batch of work to start. _timer.Dispose(); _timer = null; // If more work came in while we were running start the worker again. if (_work.Count > 0 && !_solutionIsClosing) { StartWorker(); } } OnCompletedBackgroundWork(); } catch (Exception ex) { // This is something totally unexpected, let's just send it over to the workspace. ReportError(ex); _timer?.Dispose(); _timer = null; } }
public EditorDocument( EditorDocumentManager documentManager, ProjectSnapshotManagerDispatcher projectSnapshotManagerDispatcher, JoinableTaskContext joinableTaskContext, string projectFilePath, string documentFilePath, TextLoader textLoader, FileChangeTracker fileTracker, ITextBuffer textBuffer, EventHandler changedOnDisk, EventHandler changedInEditor, EventHandler opened, EventHandler closed) { if (documentManager is null) { throw new ArgumentNullException(nameof(documentManager)); } if (projectSnapshotManagerDispatcher is null) { throw new ArgumentNullException(nameof(projectSnapshotManagerDispatcher)); } if (joinableTaskContext is null) { throw new ArgumentNullException(nameof(joinableTaskContext)); } if (projectFilePath is null) { throw new ArgumentNullException(nameof(projectFilePath)); } if (documentFilePath is null) { throw new ArgumentNullException(nameof(documentFilePath)); } if (textLoader is null) { throw new ArgumentNullException(nameof(textLoader)); } if (fileTracker is null) { throw new ArgumentNullException(nameof(fileTracker)); } _documentManager = documentManager; _projectSnapshotManagerDispatcher = projectSnapshotManagerDispatcher; _joinableTaskContext = joinableTaskContext; ProjectFilePath = projectFilePath; DocumentFilePath = documentFilePath; TextLoader = textLoader; _fileTracker = fileTracker; _changedOnDisk = changedOnDisk; _changedInEditor = changedInEditor; _opened = opened; _closed = closed; _snapshotTracker = new SnapshotChangeTracker(); _fileTracker.Changed += ChangeTracker_Changed; // Only one of these should be active at a time. if (textBuffer == null) { _ = _projectSnapshotManagerDispatcher.RunOnDispatcherThreadAsync( () => _fileTracker.StartListening(), CancellationToken.None).ConfigureAwait(false); } else { _snapshotTracker.StartTracking(textBuffer); EditorTextBuffer = textBuffer; EditorTextContainer = textBuffer.AsTextContainer(); EditorTextContainer.TextChanged += TextContainer_Changed; } }
protected Task UpdateAsync(Action action, CancellationToken cancellationToken) => _projectSnapshotManagerDispatcher.RunOnDispatcherThreadAsync(action, cancellationToken);
public async Task <RazorLanguageQueryResponse> Handle(RazorLanguageQueryParams request, CancellationToken cancellationToken) { int?documentVersion = null; DocumentSnapshot documentSnapshot = null; await _projectSnapshotManagerDispatcher.RunOnDispatcherThreadAsync(() => { _documentResolver.TryResolveDocument(request.Uri.GetAbsoluteOrUNCPath(), out documentSnapshot); Debug.Assert(documentSnapshot != null, "Failed to get the document snapshot, could not map to document ranges."); if (!_documentVersionCache.TryGetDocumentVersion(documentSnapshot, out documentVersion)) { // This typically happens for closed documents. documentVersion = null; } return(documentSnapshot); }, cancellationToken).ConfigureAwait(false); var codeDocument = await documentSnapshot.GetGeneratedOutputAsync(); var sourceText = await documentSnapshot.GetTextAsync(); var linePosition = new LinePosition(request.Position.Line, request.Position.Character); var hostDocumentIndex = sourceText.Lines.GetPosition(linePosition); var responsePosition = request.Position; if (codeDocument.IsUnsupported()) { // All language queries on unsupported documents return Html. This is equivalent to what pre-VSCode Razor was capable of. return(new RazorLanguageQueryResponse() { Kind = RazorLanguageKind.Html, Position = responsePosition, PositionIndex = hostDocumentIndex, HostDocumentVersion = documentVersion, }); } var responsePositionIndex = hostDocumentIndex; var languageKind = _documentMappingService.GetLanguageKind(codeDocument, hostDocumentIndex); if (languageKind == RazorLanguageKind.CSharp) { if (_documentMappingService.TryMapToProjectedDocumentPosition(codeDocument, hostDocumentIndex, out var projectedPosition, out var projectedIndex)) { // For C# locations, we attempt to return the corresponding position // within the projected document responsePosition = projectedPosition; responsePositionIndex = projectedIndex; } else { // It no longer makes sense to think of this location as C#, since it doesn't // correspond to any position in the projected document. This should not happen // since there should be source mappings for all the C# spans. languageKind = RazorLanguageKind.Razor; responsePositionIndex = hostDocumentIndex; } }
private async Task DocumentClosedTimer_TickAsync(CancellationToken cancellationToken) { await _projectSnapshotManagerDispatcher.RunOnDispatcherThreadAsync( ClearClosedDocuments, cancellationToken).ConfigureAwait(false); }
private async Task UpdateDocumentAsync(int newVersion, DocumentSnapshot documentSnapshot) { await ProjectSnapshotManagerDispatcher.RunOnDispatcherThreadAsync( () => VersionCache.TrackDocumentVersion(documentSnapshot, newVersion), CancellationToken.None).ConfigureAwait(false); }