From 58796a8480943f15b8b6a04f534a58db5ecd34e8 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Wed, 22 May 2024 19:45:43 +0200 Subject: [PATCH] tasks: Expose captured variables to ContextProvider (#12134) This PR changes the interface of ContextProvider, allowing it to inspect *all* variables set so far during the process of building `TaskVariables`. This makes it possible to capture e.g. an identifier in tree-sitter query, process it and then export it as a task variable. Notably, the list of variables includes captures prefixed with leading underscore; they are removed after all calls to `build_context`, but it makes it possible to capture something and then conditionally preserve it (and perhaps modify it). Release Notes: - N/A --- crates/editor/src/editor.rs | 65 ++++++++------- crates/editor/src/tasks.rs | 80 ++++++++---------- crates/extension/src/extension_store.rs | 4 +- crates/language/src/buffer.rs | 1 + crates/language/src/language.rs | 6 +- crates/language/src/task_context.rs | 104 +---------------------- crates/languages/src/bash.rs | 2 +- crates/languages/src/go.rs | 7 +- crates/languages/src/python.rs | 3 +- crates/languages/src/rust.rs | 2 +- crates/project/src/project.rs | 4 +- crates/project/src/task_inventory.rs | 105 +++++++++++++++++++++++- crates/task/src/lib.rs | 14 ++++ crates/tasks_ui/src/lib.rs | 12 +-- crates/tasks_ui/src/modal.rs | 4 +- 15 files changed, 212 insertions(+), 201 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 9c9de92aa3..cf81a2c538 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -406,6 +406,7 @@ struct RunnableTasks { templates: Vec<(TaskSourceKind, TaskTemplate)>, // We need the column at which the task context evaluation should take place. column: u32, + // Values of all named captures, including those starting with '_' extra_variables: HashMap, } @@ -3973,7 +3974,7 @@ impl Editor { this.completion_tasks.clear(); this.discard_inline_completion(false, cx); - let task_context = tasks.as_ref().zip(this.workspace.clone()).and_then( + let tasks = tasks.as_ref().zip(this.workspace.clone()).and_then( |(tasks, (workspace, _))| { let position = Point::new(buffer_row, tasks.1.column); let range_start = buffer.read(cx).anchor_at(position, Bias::Right); @@ -3981,43 +3982,45 @@ impl Editor { buffer: buffer.clone(), range: range_start..range_start, }; + // Fill in the environmental variables from the tree-sitter captures + let mut captured_task_variables = TaskVariables::default(); + for (capture_name, value) in tasks.1.extra_variables.clone() { + captured_task_variables.insert( + task::VariableName::Custom(capture_name.into()), + value.clone(), + ); + } + workspace .update(cx, |workspace, cx| { - tasks::task_context_for_location(workspace, location, cx) + tasks::task_context_for_location( + captured_task_variables, + workspace, + location, + cx, + ) }) .ok() .flatten() + .map(|task_context| { + Arc::new(ResolvedTasks { + templates: tasks + .1 + .templates + .iter() + .filter_map(|(kind, template)| { + template + .resolve_task(&kind.to_id_base(), &task_context) + .map(|task| (kind.clone(), task)) + }) + .collect(), + position: snapshot.buffer_snapshot.anchor_before( + Point::new(multibuffer_point.row, tasks.1.column), + ), + }) + }) }, ); - let tasks = tasks.zip(task_context).map(|(tasks, mut task_context)| { - // Fill in the environmental variables from the tree-sitter captures - let mut additional_task_variables = TaskVariables::default(); - for (capture_name, value) in tasks.1.extra_variables.clone() { - additional_task_variables.insert( - task::VariableName::Custom(capture_name.into()), - value.clone(), - ); - } - task_context - .task_variables - .extend(additional_task_variables); - - Arc::new(ResolvedTasks { - templates: tasks - .1 - .templates - .iter() - .filter_map(|(kind, template)| { - template - .resolve_task(&kind.to_id_base(), &task_context) - .map(|task| (kind.clone(), task)) - }) - .collect(), - position: snapshot - .buffer_snapshot - .anchor_before(Point::new(multibuffer_point.row, tasks.1.column)), - }) - }); let spawn_straight_away = tasks .as_ref() .map_or(false, |tasks| tasks.templates.len() == 1) diff --git a/crates/editor/src/tasks.rs b/crates/editor/src/tasks.rs index 7fe695b251..73ab73f26a 100644 --- a/crates/editor/src/tasks.rs +++ b/crates/editor/src/tasks.rs @@ -1,17 +1,16 @@ use crate::Editor; -use std::{path::Path, sync::Arc}; - use anyhow::Context; -use gpui::WindowContext; -use language::{BasicContextProvider, ContextProvider}; -use project::{Location, WorktreeId}; +use gpui::{Model, WindowContext}; +use language::ContextProvider; +use project::{BasicContextProvider, Location, Project}; use task::{TaskContext, TaskVariables, VariableName}; use text::Point; use util::ResultExt; use workspace::Workspace; pub(crate) fn task_context_for_location( + captured_variables: TaskVariables, workspace: &Workspace, location: Location, cx: &mut WindowContext<'_>, @@ -20,31 +19,16 @@ pub(crate) fn task_context_for_location( .log_err() .flatten(); - let buffer = location.buffer.clone(); - let language_context_provider = buffer - .read(cx) - .language() - .and_then(|language| language.context_provider()) - .unwrap_or_else(|| Arc::new(BasicContextProvider)); - - let worktree_abs_path = buffer - .read(cx) - .file() - .map(|file| WorktreeId::from_usize(file.worktree_id())) - .and_then(|worktree_id| { - workspace - .project() - .read(cx) - .worktree_for_id(worktree_id, cx) - .map(|worktree| worktree.read(cx).abs_path()) - }); - let task_variables = combine_task_variables( - worktree_abs_path.as_deref(), + let mut task_variables = combine_task_variables( + captured_variables, location, - language_context_provider.as_ref(), + workspace.project().clone(), cx, ) .log_err()?; + // Remove all custom entries starting with _, as they're not intended for use by the end user. + task_variables.sweep(); + Some(TaskContext { cwd, task_variables, @@ -84,21 +68,21 @@ fn task_context_with_editor( buffer, range: start..end, }; - task_context_for_location(workspace, location.clone(), cx).map(|mut task_context| { + let captured_variables = { + let mut variables = TaskVariables::default(); for range in location .buffer .read(cx) .snapshot() - .runnable_ranges(location.range) + .runnable_ranges(location.range.clone()) { for (capture_name, value) in range.extra_captures { - task_context - .task_variables - .insert(VariableName::Custom(capture_name.into()), value); + variables.insert(VariableName::Custom(capture_name.into()), value); } } - task_context - }) + variables + }; + task_context_for_location(captured_variables, workspace, location.clone(), cx) } pub fn task_context(workspace: &Workspace, cx: &mut WindowContext<'_>) -> TaskContext { @@ -114,24 +98,26 @@ pub fn task_context(workspace: &Workspace, cx: &mut WindowContext<'_>) -> TaskCo } fn combine_task_variables( - worktree_abs_path: Option<&Path>, + mut captured_variables: TaskVariables, location: Location, - context_provider: &dyn ContextProvider, + project: Model, cx: &mut WindowContext<'_>, ) -> anyhow::Result { - if context_provider.is_basic() { - context_provider - .build_context(worktree_abs_path, &location, cx) - .context("building basic provider context") - } else { - let mut basic_context = BasicContextProvider - .build_context(worktree_abs_path, &location, cx) - .context("building basic default context")?; - basic_context.extend( - context_provider - .build_context(worktree_abs_path, &location, cx) + let language_context_provider = location + .buffer + .read(cx) + .language() + .and_then(|language| language.context_provider()); + let baseline = BasicContextProvider::new(project) + .build_context(&captured_variables, &location, cx) + .context("building basic default context")?; + captured_variables.extend(baseline); + if let Some(provider) = language_context_provider { + captured_variables.extend( + provider + .build_context(&captured_variables, &location, cx) .context("building provider context ")?, ); - Ok(basic_context) } + Ok(captured_variables) } diff --git a/crates/extension/src/extension_store.rs b/crates/extension/src/extension_store.rs index 2f220b66e8..5acdcdb816 100644 --- a/crates/extension/src/extension_store.rs +++ b/crates/extension/src/extension_store.rs @@ -30,10 +30,10 @@ use gpui::{ }; use http::{AsyncBody, HttpClient, HttpClientWithUrl}; use language::{ - ContextProviderWithTasks, LanguageConfig, LanguageMatcher, LanguageQueries, LanguageRegistry, - QUERY_FILENAME_PREFIXES, + LanguageConfig, LanguageMatcher, LanguageQueries, LanguageRegistry, QUERY_FILENAME_PREFIXES, }; use node_runtime::NodeRuntime; +use project::ContextProviderWithTasks; use semantic_version::SemanticVersion; use serde::{Deserialize, Serialize}; use settings::Settings; diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 96ba71f4b0..13cfe2b0e1 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -3024,6 +3024,7 @@ impl BufferSnapshot { tags.sort_by_key(|(range, _)| range == &maximum_range); let split_point = tags.partition_point(|(range, _)| range != &maximum_range); let (extra_captures, tags) = tags.split_at(split_point); + let extra_captures = extra_captures .into_iter() .map(|(range, name)| { diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index a5826c2555..99f01456c9 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -58,9 +58,7 @@ use std::{ }; use syntax_map::{QueryCursorHandle, SyntaxSnapshot}; use task::RunnableTag; -pub use task_context::{ - BasicContextProvider, ContextProvider, ContextProviderWithTasks, RunnableRange, -}; +pub use task_context::{ContextProvider, RunnableRange}; use theme::SyntaxTheme; use tree_sitter::{self, wasmtime, Query, QueryCursor, WasmStore}; @@ -1016,7 +1014,7 @@ impl Language { for (ix, name) in query.capture_names().iter().enumerate() { if *name == "run" { run_capture_index = Some(ix as u32); - } else if !name.starts_with('_') { + } else { runnable_tags.insert(ix as u32, RunnableTag(name.to_string().into())); } } diff --git a/crates/language/src/task_context.rs b/crates/language/src/task_context.rs index 2afeb198c7..a290c64c89 100644 --- a/crates/language/src/task_context.rs +++ b/crates/language/src/task_context.rs @@ -1,12 +1,12 @@ -use std::{ops::Range, path::Path}; +use std::ops::Range; use crate::{Location, Runnable}; use anyhow::Result; use collections::HashMap; use gpui::AppContext; -use task::{TaskTemplates, TaskVariables, VariableName}; -use text::{BufferId, Point, ToPoint}; +use task::{TaskTemplates, TaskVariables}; +use text::BufferId; pub struct RunnableRange { pub buffer_id: BufferId, @@ -22,7 +22,7 @@ pub trait ContextProvider: Send + Sync { /// Builds a specific context to be placed on top of the basic one (replacing all conflicting entries) and to be used for task resolving later. fn build_context( &self, - _worktree_abs_path: Option<&Path>, + _variables: &TaskVariables, _location: &Location, _cx: &mut AppContext, ) -> Result { @@ -33,100 +33,4 @@ pub trait ContextProvider: Send + Sync { fn associated_tasks(&self) -> Option { None } - - // Determines whether the [`BasicContextProvider`] variables should be filled too (if `false`), or omitted (if `true`). - fn is_basic(&self) -> bool { - false - } -} - -/// A context provided that tries to provide values for all non-custom [`VariableName`] variants for a currently opened file. -/// Applied as a base for every custom [`ContextProvider`] unless explicitly oped out. -pub struct BasicContextProvider; - -impl ContextProvider for BasicContextProvider { - fn is_basic(&self) -> bool { - true - } - - fn build_context( - &self, - worktree_abs_path: Option<&Path>, - location: &Location, - cx: &mut AppContext, - ) -> Result { - let buffer = location.buffer.read(cx); - let buffer_snapshot = buffer.snapshot(); - let symbols = buffer_snapshot.symbols_containing(location.range.start, None); - let symbol = symbols.unwrap_or_default().last().map(|symbol| { - let range = symbol - .name_ranges - .last() - .cloned() - .unwrap_or(0..symbol.text.len()); - symbol.text[range].to_string() - }); - - let current_file = buffer - .file() - .and_then(|file| file.as_local()) - .map(|file| file.abs_path(cx).to_string_lossy().to_string()); - let Point { row, column } = location.range.start.to_point(&buffer_snapshot); - let row = row + 1; - let column = column + 1; - let selected_text = buffer - .chars_for_range(location.range.clone()) - .collect::(); - - let mut task_variables = TaskVariables::from_iter([ - (VariableName::Row, row.to_string()), - (VariableName::Column, column.to_string()), - ]); - - if let Some(symbol) = symbol { - task_variables.insert(VariableName::Symbol, symbol); - } - if !selected_text.trim().is_empty() { - task_variables.insert(VariableName::SelectedText, selected_text); - } - if let Some(path) = current_file { - task_variables.insert(VariableName::File, path); - } - if let Some(worktree_path) = worktree_abs_path { - task_variables.insert( - VariableName::WorktreeRoot, - worktree_path.to_string_lossy().to_string(), - ); - } - - Ok(task_variables) - } -} - -/// A ContextProvider that doesn't provide any task variables on it's own, though it has some associated tasks. -pub struct ContextProviderWithTasks { - templates: TaskTemplates, -} - -impl ContextProviderWithTasks { - pub fn new(definitions: TaskTemplates) -> Self { - Self { - templates: definitions, - } - } -} - -impl ContextProvider for ContextProviderWithTasks { - fn associated_tasks(&self) -> Option { - Some(self.templates.clone()) - } - - fn build_context( - &self, - worktree_abs_path: Option<&Path>, - location: &Location, - cx: &mut AppContext, - ) -> Result { - BasicContextProvider.build_context(worktree_abs_path, location, cx) - } } diff --git a/crates/languages/src/bash.rs b/crates/languages/src/bash.rs index 5bca10aee4..bc4b7be467 100644 --- a/crates/languages/src/bash.rs +++ b/crates/languages/src/bash.rs @@ -1,4 +1,4 @@ -use language::ContextProviderWithTasks; +use project::ContextProviderWithTasks; use task::{TaskTemplate, TaskTemplates, VariableName}; pub(super) fn bash_task_context() -> ContextProviderWithTasks { diff --git a/crates/languages/src/go.rs b/crates/languages/src/go.rs index 85c92323c4..8b35220234 100644 --- a/crates/languages/src/go.rs +++ b/crates/languages/src/go.rs @@ -16,7 +16,7 @@ use std::{ borrow::Cow, ffi::{OsStr, OsString}, ops::Range, - path::{Path, PathBuf}, + path::PathBuf, str, sync::{ atomic::{AtomicBool, Ordering::SeqCst}, @@ -447,7 +447,7 @@ const GO_PACKAGE_TASK_VARIABLE: VariableName = VariableName::Custom(Cow::Borrowe impl ContextProvider for GoContextProvider { fn build_context( &self, - worktree_abs_path: Option<&Path>, + variables: &TaskVariables, location: &Location, cx: &mut gpui::AppContext, ) -> Result { @@ -465,7 +465,8 @@ impl ContextProvider for GoContextProvider { // Prefer the relative form `./my-nested-package/is-here` over // absolute path, because it's more readable in the modal, but // the absolute path also works. - let package_name = worktree_abs_path + let package_name = variables + .get(&VariableName::WorktreeRoot) .and_then(|worktree_abs_path| buffer_dir.strip_prefix(worktree_abs_path).ok()) .map(|relative_pkg_dir| { if relative_pkg_dir.as_os_str().is_empty() { diff --git a/crates/languages/src/python.rs b/crates/languages/src/python.rs index 0f5e0367f5..86a83ef6f9 100644 --- a/crates/languages/src/python.rs +++ b/crates/languages/src/python.rs @@ -1,8 +1,9 @@ use anyhow::Result; use async_trait::async_trait; -use language::{ContextProviderWithTasks, LanguageServerName, LspAdapter, LspAdapterDelegate}; +use language::{LanguageServerName, LspAdapter, LspAdapterDelegate}; use lsp::LanguageServerBinary; use node_runtime::NodeRuntime; +use project::ContextProviderWithTasks; use std::{ any::Any, ffi::OsString, diff --git a/crates/languages/src/rust.rs b/crates/languages/src/rust.rs index 72566e3ba3..8b2e48ff15 100644 --- a/crates/languages/src/rust.rs +++ b/crates/languages/src/rust.rs @@ -328,7 +328,7 @@ const RUST_PACKAGE_TASK_VARIABLE: VariableName = impl ContextProvider for RustContextProvider { fn build_context( &self, - _: Option<&Path>, + _: &TaskVariables, location: &Location, cx: &mut gpui::AppContext, ) -> Result { diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 1b8e1cac4e..1fac90d7fb 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -113,7 +113,9 @@ pub use fs::*; pub use language::Location; #[cfg(any(test, feature = "test-support"))] pub use prettier::FORMAT_SUFFIX as TEST_PRETTIER_FORMAT_SUFFIX; -pub use task_inventory::{Inventory, TaskSourceKind}; +pub use task_inventory::{ + BasicContextProvider, ContextProviderWithTasks, Inventory, TaskSourceKind, +}; pub use worktree::{ DiagnosticSummary, Entry, EntryKind, File, LocalWorktree, PathChange, ProjectEntryId, RepositoryEntry, UpdatedEntriesSet, UpdatedGitRepositoriesSet, Worktree, WorktreeId, diff --git a/crates/project/src/task_inventory.rs b/crates/project/src/task_inventory.rs index 66e2b9cea8..9f16de1f92 100644 --- a/crates/project/src/task_inventory.rs +++ b/crates/project/src/task_inventory.rs @@ -6,6 +6,7 @@ use std::{ sync::Arc, }; +use anyhow::Result; use collections::{btree_map, BTreeMap, VecDeque}; use futures::{ channel::mpsc::{unbounded, UnboundedSender}, @@ -13,13 +14,17 @@ use futures::{ }; use gpui::{AppContext, Context, Model, ModelContext, Task}; use itertools::Itertools; -use language::Language; +use language::{ContextProvider, Language, Location}; use task::{ - static_source::StaticSource, ResolvedTask, TaskContext, TaskId, TaskTemplate, VariableName, + static_source::StaticSource, ResolvedTask, TaskContext, TaskId, TaskTemplate, TaskTemplates, + TaskVariables, VariableName, }; +use text::{Point, ToPoint}; use util::{post_inc, NumericPrefixWithSuffix}; use worktree::WorktreeId; +use crate::Project; + /// Inventory tracks available tasks for a given project. pub struct Inventory { sources: Vec, @@ -491,6 +496,102 @@ mod test_inventory { } } +/// A context provided that tries to provide values for all non-custom [`VariableName`] variants for a currently opened file. +/// Applied as a base for every custom [`ContextProvider`] unless explicitly oped out. +pub struct BasicContextProvider { + project: Model, +} + +impl BasicContextProvider { + pub fn new(project: Model) -> Self { + Self { project } + } +} + +impl ContextProvider for BasicContextProvider { + fn build_context( + &self, + _: &TaskVariables, + location: &Location, + cx: &mut AppContext, + ) -> Result { + let buffer = location.buffer.read(cx); + let buffer_snapshot = buffer.snapshot(); + let symbols = buffer_snapshot.symbols_containing(location.range.start, None); + let symbol = symbols.unwrap_or_default().last().map(|symbol| { + let range = symbol + .name_ranges + .last() + .cloned() + .unwrap_or(0..symbol.text.len()); + symbol.text[range].to_string() + }); + + let current_file = buffer + .file() + .and_then(|file| file.as_local()) + .map(|file| file.abs_path(cx).to_string_lossy().to_string()); + let Point { row, column } = location.range.start.to_point(&buffer_snapshot); + let row = row + 1; + let column = column + 1; + let selected_text = buffer + .chars_for_range(location.range.clone()) + .collect::(); + + let mut task_variables = TaskVariables::from_iter([ + (VariableName::Row, row.to_string()), + (VariableName::Column, column.to_string()), + ]); + + if let Some(symbol) = symbol { + task_variables.insert(VariableName::Symbol, symbol); + } + if !selected_text.trim().is_empty() { + task_variables.insert(VariableName::SelectedText, selected_text); + } + if let Some(path) = current_file { + task_variables.insert(VariableName::File, path); + } + + let worktree_abs_path = buffer + .file() + .map(|file| WorktreeId::from_usize(file.worktree_id())) + .and_then(|worktree_id| { + self.project + .read(cx) + .worktree_for_id(worktree_id, cx) + .map(|worktree| worktree.read(cx).abs_path()) + }); + if let Some(worktree_path) = worktree_abs_path { + task_variables.insert( + VariableName::WorktreeRoot, + worktree_path.to_string_lossy().to_string(), + ); + } + + Ok(task_variables) + } +} + +/// A ContextProvider that doesn't provide any task variables on it's own, though it has some associated tasks. +pub struct ContextProviderWithTasks { + templates: TaskTemplates, +} + +impl ContextProviderWithTasks { + pub fn new(definitions: TaskTemplates) -> Self { + Self { + templates: definitions, + } + } +} + +impl ContextProvider for ContextProviderWithTasks { + fn associated_tasks(&self) -> Option { + Some(self.templates.clone()) + } +} + #[cfg(test)] mod tests { use gpui::TestAppContext; diff --git a/crates/task/src/lib.rs b/crates/task/src/lib.rs index bdd4ca48d6..d1c9aa22cc 100644 --- a/crates/task/src/lib.rs +++ b/crates/task/src/lib.rs @@ -185,6 +185,20 @@ impl TaskVariables { pub fn extend(&mut self, other: Self) { self.0.extend(other.0); } + /// Get the value associated with given variable name, if there is one. + pub fn get(&self, key: &VariableName) -> Option<&str> { + self.0.get(key).map(|s| s.as_str()) + } + /// Clear out variables obtained from tree-sitter queries, which are prefixed with '_' character + pub fn sweep(&mut self) { + self.0.retain(|name, _| { + if let VariableName::Custom(name) = name { + !name.starts_with('_') + } else { + true + } + }) + } } impl FromIterator<(VariableName, String)> for TaskVariables { diff --git a/crates/tasks_ui/src/lib.rs b/crates/tasks_ui/src/lib.rs index 20a72e37de..8b91ccd268 100644 --- a/crates/tasks_ui/src/lib.rs +++ b/crates/tasks_ui/src/lib.rs @@ -153,8 +153,8 @@ mod tests { use editor::Editor; use gpui::{Entity, TestAppContext}; - use language::{BasicContextProvider, Language, LanguageConfig}; - use project::{FakeFs, Project}; + use language::{Language, LanguageConfig}; + use project::{BasicContextProvider, FakeFs, Project}; use serde_json::json; use task::{TaskContext, TaskVariables, VariableName}; use ui::VisualContext; @@ -191,7 +191,7 @@ mod tests { }), ) .await; - + let project = Project::test(fs, ["/dir".as_ref()], cx).await; let rust_language = Arc::new( Language::new( LanguageConfig::default(), @@ -203,7 +203,7 @@ mod tests { name: (_) @name) @item"#, ) .unwrap() - .with_context_provider(Some(Arc::new(BasicContextProvider))), + .with_context_provider(Some(Arc::new(BasicContextProvider::new(project.clone())))), ); let typescript_language = Arc::new( @@ -221,9 +221,9 @@ mod tests { ")" @context)) @item"#, ) .unwrap() - .with_context_provider(Some(Arc::new(BasicContextProvider))), + .with_context_provider(Some(Arc::new(BasicContextProvider::new(project.clone())))), ); - let project = Project::test(fs, ["/dir".as_ref()], cx).await; + let worktree_id = project.update(cx, |project, cx| { project.worktrees().next().unwrap().read(cx).id() }); diff --git a/crates/tasks_ui/src/modal.rs b/crates/tasks_ui/src/modal.rs index 8d8024e272..f6d75d3b0b 100644 --- a/crates/tasks_ui/src/modal.rs +++ b/crates/tasks_ui/src/modal.rs @@ -540,8 +540,8 @@ mod tests { use editor::Editor; use gpui::{TestAppContext, VisualTestContext}; - use language::{ContextProviderWithTasks, Language, LanguageConfig, LanguageMatcher, Point}; - use project::{FakeFs, Project}; + use language::{Language, LanguageConfig, LanguageMatcher, Point}; + use project::{ContextProviderWithTasks, FakeFs, Project}; use serde_json::json; use task::TaskTemplates; use workspace::CloseInactiveTabsAndPanes;