Compare commits

...

15 Commits

Author SHA1 Message Date
Piotr Osiewicz
6486bb7cc4 Add elided lifetime to make merging with code-actions seamless 2024-04-26 17:29:56 +02:00
Piotr Osiewicz
de0c8bc368 Remove moot import 2024-04-26 17:20:33 +02:00
Piotr Osiewicz
af555a6189 Get rid of cx and TaskSource trait 2024-04-26 17:16:27 +02:00
Piotr Osiewicz
e4781ce5fa Remove TestTaskSource 2024-04-26 15:31:39 +02:00
Piotr Osiewicz
c128c3ac79 fix clippy lint 2024-04-26 13:22:51 +02:00
Piotr Osiewicz
893b0eb371 Strip unused deps 2024-04-26 13:21:49 +02:00
Piotr Osiewicz
8d05ee91b3 Fix up build and a bunch of warnings 2024-04-26 13:12:46 +02:00
Mikayla
4abd55d340 Finish collecting data to be able to spawn tasks 2024-04-25 16:52:01 -07:00
Piotr Osiewicz
8aac108d5a fixup! Wire through inventory querying 2024-04-25 16:52:01 -07:00
Piotr Osiewicz
3d41adfc53 Wire through inventory querying
Co-authored-by: Mikayla <mikayla@zed.dev>
2024-04-25 16:52:01 -07:00
Piotr Osiewicz
dee6b5629f fixup! Fix up CI 2024-04-25 16:52:01 -07:00
Piotr Osiewicz
266949d56b Fix up CI 2024-04-25 16:52:01 -07:00
Mikayla
fade2f4843 WIP: Get git blame menu rendering 2024-04-25 16:52:01 -07:00
Mikayla
e5f9780c05 add tree sitter query implementation 2024-04-25 16:52:01 -07:00
Mikayla
a60785c423 Wire in new language query
Restore live updating language queries
2024-04-25 16:52:01 -07:00
27 changed files with 816 additions and 539 deletions

3
Cargo.lock generated
View File

@@ -3390,6 +3390,7 @@ dependencies = [
"smol",
"snippet",
"sum_tree",
"task",
"text",
"theme",
"time",
@@ -9794,6 +9795,7 @@ dependencies = [
"futures 0.3.28",
"gpui",
"hex",
"parking_lot",
"schemars",
"serde",
"serde_json_lenient",
@@ -9806,7 +9808,6 @@ dependencies = [
name = "tasks_ui"
version = "0.1.0"
dependencies = [
"anyhow",
"editor",
"file_icons",
"fuzzy",

View File

@@ -60,6 +60,7 @@ smallvec.workspace = true
smol.workspace = true
snippet.workspace = true
sum_tree.workspace = true
task.workspace = true
text.workspace = true
time.workspace = true
time_format.workspace = true

View File

@@ -43,6 +43,12 @@ pub struct ToggleCodeActions {
pub deployed_from_indicator: bool,
}
#[derive(PartialEq, Clone, Deserialize, Default)]
pub struct ToggleTestRunner {
#[serde(default)]
pub deployed_from_indicator: Option<u32>,
}
#[derive(PartialEq, Clone, Deserialize, Default)]
pub struct ConfirmCompletion {
#[serde(default)]

View File

@@ -33,6 +33,7 @@ mod persistence;
mod rust_analyzer_ext;
pub mod scroll;
mod selections_collection;
pub mod tasks;
#[cfg(test)]
mod editor_tests;
@@ -75,6 +76,7 @@ use inlay_hint_cache::{InlayHintCache, InlaySplice, InvalidationStrategy};
pub use inline_completion_provider::*;
pub use items::MAX_TAB_TITLE_LEN;
use itertools::Itertools;
use language::Runnable;
use language::{
char_kind,
language_settings::{self, all_language_settings, InlayHintSettings},
@@ -82,6 +84,7 @@ use language::{
CursorShape, Diagnostic, Documentation, IndentKind, IndentSize, Language, OffsetRangeExt,
Point, Selection, SelectionGoal, TransactionId,
};
use task::TaskTemplate;
use hover_links::{HoverLink, HoveredLinkState, InlayHighlight};
use lsp::{DiagnosticSeverity, LanguageServerId};
@@ -96,7 +99,8 @@ use ordered_float::OrderedFloat;
use parking_lot::{Mutex, RwLock};
use project::project_settings::{GitGutterSetting, ProjectSettings};
use project::{
CodeAction, Completion, FormatTrigger, Item, Location, Project, ProjectPath, ProjectTransaction,
CodeAction, Completion, FormatTrigger, Item, Location, Project, ProjectPath,
ProjectTransaction, WorktreeId,
};
use rand::prelude::*;
use rpc::proto::*;
@@ -388,6 +392,13 @@ impl Default for ScrollbarMarkerState {
}
}
struct RunnableTasks {
templates: SmallVec<[TaskTemplate; 1]>,
match_range: Range<usize>, // The equivalent of the newest selection,
language: Arc<Language>, // For getting a context provider
worktree: Option<WorktreeId>,
}
/// Zed's primary text input `View`, allowing users to edit a [`MultiBuffer`]
///
/// See the [module level documentation](self) for more information.
@@ -3677,6 +3688,47 @@ impl Editor {
.detach_and_log_err(cx);
}
pub fn toggle_test_runner(&mut self, action: &ToggleTestRunner, cx: &mut ViewContext<Self>) {
unimplemented!()
// let mut context_menu = self.context_menu.write();
// if matches!(context_menu.as_ref(), Some(ContextMenu::CodeActions(_))) {
// *context_menu = None;
// cx.notify();
// return;
// }
// drop(context_menu);
// let deployed_from_indicator = action.deployed_from_indicator;
// let mut task = self.code_actions_task.take();
// cx.spawn(|this, mut cx| async move {
// while let Some(prev_task) = task {
// prev_task.await;
// task = this.update(&mut cx, |this, _| this.code_actions_task.take())?;
// }
// this.update(&mut cx, |this, cx| {
// if this.focus_handle.is_focused(cx) {
// if let Some((buffer, actions)) = this.available_code_actions.clone() {
// this.completion_tasks.clear();
// this.discard_inline_completion(cx);
// *this.context_menu.write() =
// Some(ContextMenu::CodeActions(CodeActionsMenu {
// buffer,
// actions,
// selected_item: Default::default(),
// scroll_handle: UniformListScrollHandle::default(),
// deployed_from_indicator,
// }));
// cx.notify();
// }
// }
// })?;
// Ok::<_, anyhow::Error>(())
// })
// .detach_and_log_err(cx);
}
pub fn confirm_code_action(
&mut self,
action: &ConfirmCodeAction,
@@ -4199,6 +4251,28 @@ impl Editor {
}
}
pub fn render_test_run_indicator(
&self,
_style: &EditorStyle,
is_active: bool,
indicator: u32,
cx: &mut ViewContext<Self>,
) -> IconButton {
IconButton::new("code_actions_indicator", ui::IconName::Play)
.icon_size(IconSize::XSmall)
.size(ui::ButtonSize::None)
.icon_color(Color::Muted)
.selected(is_active)
.on_click(cx.listener(move |editor, _e, cx| {
editor.toggle_test_runner(
&ToggleTestRunner {
deployed_from_indicator: Some(indicator),
},
cx,
);
}))
}
pub fn render_fold_indicators(
&mut self,
fold_data: Vec<Option<(FoldStatus, u32, bool)>>,
@@ -7375,6 +7449,72 @@ impl Editor {
self.select_larger_syntax_node_stack = stack;
}
fn runnable_display_rows(
&self,
range: Range<Anchor>,
snapshot: &DisplaySnapshot,
cx: &WindowContext,
) -> Vec<(u32, RunnableTasks)> {
snapshot
.buffer_snapshot
.runnable_ranges(range)
.filter_map(|(multi_buffer_range, mut runnable)| {
let (tasks, worktree_id) = self.resolve_runnable(&mut runnable, cx);
if tasks.is_empty() {
return None;
}
Some((
multi_buffer_range.start.to_display_point(&snapshot).row(),
RunnableTasks {
templates: tasks,
match_range: multi_buffer_range,
language: runnable.language,
worktree: worktree_id,
},
))
})
.collect()
}
fn resolve_runnable(
&self,
runnable: &mut Runnable,
cx: &WindowContext<'_>,
) -> (SmallVec<[TaskTemplate; 1]>, Option<WorktreeId>) {
let Some(project) = self.project.as_ref() else {
return Default::default();
};
let (inventory, worktree_id) = project.read_with(cx, |project, cx| {
let worktree_id = project
.buffer_for_id(runnable.buffer)
.and_then(|buffer| buffer.read(cx).file())
.map(|file| WorktreeId::from_usize(file.worktree_id()));
(project.task_inventory().clone(), worktree_id)
});
let inventory = inventory.read(cx);
let tags = mem::take(&mut runnable.tags);
(
SmallVec::from_iter(
tags.into_iter()
.flat_map(|tag| {
let tag = tag.0.clone();
inventory
.list_tasks(Some(runnable.language.clone()), worktree_id)
.into_iter()
.filter(move |(_, template)| {
template.tags.iter().any(|source_tag| source_tag == &tag)
})
})
.sorted_by_key(|(kind, _)| kind.to_owned())
.map(|(_, template)| template),
),
worktree_id,
)
}
pub fn move_to_enclosing_bracket(
&mut self,
_: &MoveToEnclosingBracket,

View File

@@ -15,7 +15,7 @@ use crate::{
CursorShape, DisplayPoint, DocumentHighlightRead, DocumentHighlightWrite, Editor, EditorMode,
EditorSettings, EditorSnapshot, EditorStyle, ExpandExcerpts, GutterDimensions, HalfPageDown,
HalfPageUp, HoveredCursor, LineDown, LineUp, OpenExcerpts, PageDown, PageUp, Point,
SelectPhase, Selection, SoftWrap, ToPoint, CURSORS_VISIBLE_FOR, MAX_LINE_LEN,
RunnableTasks, SelectPhase, Selection, SoftWrap, ToPoint, CURSORS_VISIBLE_FOR, MAX_LINE_LEN,
};
use anyhow::Result;
use collections::{BTreeMap, HashMap};
@@ -49,6 +49,7 @@ use std::{
sync::Arc,
};
use sum_tree::Bias;
use task::{RunnableTag, TaskTemplate};
use theme::{ActiveTheme, PlayerColor};
use ui::prelude::*;
use ui::{h_flex, ButtonLike, ButtonStyle, ContextMenu, Tooltip};
@@ -1277,6 +1278,37 @@ impl EditorElement {
Some(shaped_lines)
}
fn layout_test_run_indicators(
&self,
test_lines: Vec<(u32, RunnableTasks)>,
line_height: Pixels,
scroll_pixel_position: gpui::Point<Pixels>,
gutter_dimensions: &GutterDimensions,
gutter_hitbox: &Hitbox,
cx: &mut WindowContext,
) -> Vec<AnyElement> {
test_lines
.into_iter()
.filter_map(|(line, tags)| {
let button = self.editor.update(cx, |editor, cx| {
// active = todo check if the run menu is open or something
editor.render_test_run_indicator(&self.style, false, line, cx)
});
let button = prepaint_gutter_button(
button,
line,
line_height,
gutter_dimensions,
scroll_pixel_position,
gutter_hitbox,
cx,
);
Some(button)
})
.collect_vec()
}
fn layout_code_actions_indicator(
&self,
line_height: Pixels,
@@ -1296,27 +1328,16 @@ impl EditorElement {
button = editor.render_code_actions_indicator(&self.style, active, cx);
});
let mut button = button?.into_any_element();
let available_space = size(
AvailableSpace::MinContent,
AvailableSpace::Definite(line_height),
let button = prepaint_gutter_button(
button?,
newest_selection_head.row(),
line_height,
gutter_dimensions,
scroll_pixel_position,
gutter_hitbox,
cx,
);
let indicator_size = button.layout_as_root(available_space, cx);
let blame_width = gutter_dimensions
.git_blame_entries_width
.unwrap_or(Pixels::ZERO);
let mut x = blame_width;
let available_width = gutter_dimensions.margin + gutter_dimensions.left_padding
- indicator_size.width
- blame_width;
x += available_width / 2.;
let mut y = newest_selection_head.row() as f32 * line_height - scroll_pixel_position.y;
y += (line_height - indicator_size.height) / 2.;
button.prepaint_as_root(gutter_hitbox.origin + point(x, y), available_space, cx);
Some(button)
}
@@ -2229,6 +2250,12 @@ impl EditorElement {
}
});
cx.with_element_id(Some("gutter_test_indicators"), |cx| {
for test_indicators in layout.test_indicators.iter_mut() {
test_indicators.paint(cx);
}
});
if let Some(indicator) = layout.code_actions_indicator.as_mut() {
indicator.paint(cx);
}
@@ -3036,6 +3063,39 @@ impl EditorElement {
}
}
fn prepaint_gutter_button(
button: IconButton,
row: u32,
line_height: Pixels,
gutter_dimensions: &GutterDimensions,
scroll_pixel_position: gpui::Point<Pixels>,
gutter_hitbox: &Hitbox,
cx: &mut WindowContext<'_>,
) -> AnyElement {
let mut button = button.into_any_element();
let available_space = size(
AvailableSpace::MinContent,
AvailableSpace::Definite(line_height),
);
let indicator_size = button.layout_as_root(available_space, cx);
let blame_width = gutter_dimensions
.git_blame_entries_width
.unwrap_or(Pixels::ZERO);
let mut x = blame_width;
let available_width = gutter_dimensions.margin + gutter_dimensions.left_padding
- indicator_size.width
- blame_width;
x += available_width / 2.;
let mut y = row as f32 * line_height - scroll_pixel_position.y;
y += (line_height - indicator_size.height) / 2.;
button.prepaint_as_root(gutter_hitbox.origin + point(x, y), available_space, cx);
button
}
fn render_inline_blame_entry(
blame: &gpui::Model<GitBlame>,
blame_entry: BlameEntry,
@@ -3550,6 +3610,12 @@ impl Element for EditorElement {
cx,
);
let test_lines = self.editor.read(cx).runnable_display_rows(
start_anchor..end_anchor,
&snapshot.display_snapshot,
cx,
);
let (selections, active_rows, newest_selection_head) = self.layout_selections(
start_anchor,
end_anchor,
@@ -3717,18 +3783,34 @@ impl Element for EditorElement {
cx,
);
if gutter_settings.code_actions {
code_actions_indicator = self.layout_code_actions_indicator(
line_height,
newest_selection_head,
scroll_pixel_position,
&gutter_dimensions,
&gutter_hitbox,
cx,
);
let has_test_indicator = test_lines
.iter()
.any(|(line, _)| *line == newest_selection_head.row());
if !has_test_indicator {
code_actions_indicator = self.layout_code_actions_indicator(
line_height,
newest_selection_head,
scroll_pixel_position,
&gutter_dimensions,
&gutter_hitbox,
cx,
);
}
}
}
}
let test_indicators = cx.with_element_id(Some("test-run"), |cx| {
self.layout_test_run_indicators(
test_lines,
line_height,
scroll_pixel_position,
&gutter_dimensions,
&gutter_hitbox,
cx,
)
});
if !context_menu_visible && !cx.has_active_drag() {
self.layout_hover_popovers(
&snapshot,
@@ -3828,6 +3910,7 @@ impl Element for EditorElement {
cursors,
selections,
mouse_context_menu,
test_indicators,
code_actions_indicator,
fold_indicators,
tab_invisible,
@@ -3918,6 +4001,7 @@ pub struct EditorLayout {
selections: Vec<(PlayerColor, Vec<SelectionLayout>)>,
max_row: u32,
code_actions_indicator: Option<AnyElement>,
test_indicators: Vec<AnyElement>,
fold_indicators: Vec<Option<AnyElement>>,
mouse_context_menu: Option<AnyElement>,
tab_invisible: ShapedLine,

View File

@@ -0,0 +1,99 @@
use crate::Editor;
use std::{path::Path, sync::Arc};
use anyhow::Context;
use gpui::WindowContext;
use language::{BasicContextProvider, ContextProvider};
use project::{Location, WorktreeId};
use task::{TaskContext, TaskVariables};
use util::ResultExt;
use workspace::Workspace;
pub fn task_context(workspace: &Workspace, cx: &mut WindowContext<'_>) -> TaskContext {
fn task_context_impl(workspace: &Workspace, cx: &mut WindowContext<'_>) -> Option<TaskContext> {
let cwd = workspace::tasks::task_cwd(workspace, cx)
.log_err()
.flatten();
let editor = workspace
.active_item(cx)
.and_then(|item| item.act_as::<Editor>(cx))?;
let (selection, buffer, editor_snapshot) = editor.update(cx, |editor, cx| {
let selection = editor.selections.newest::<usize>(cx);
let (buffer, _, _) = editor
.buffer()
.read(cx)
.point_to_buffer_offset(selection.start, cx)?;
let snapshot = editor.snapshot(cx);
Some((selection, buffer, snapshot))
})?;
let language_context_provider = buffer
.read(cx)
.language()
.and_then(|language| language.context_provider())
.unwrap_or_else(|| Arc::new(BasicContextProvider));
let selection_range = selection.range();
let start = editor_snapshot
.display_snapshot
.buffer_snapshot
.anchor_after(selection_range.start)
.text_anchor;
let end = editor_snapshot
.display_snapshot
.buffer_snapshot
.anchor_after(selection_range.end)
.text_anchor;
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 location = Location {
buffer,
range: start..end,
};
let task_variables = combine_task_variables(
worktree_abs_path.as_deref(),
location,
language_context_provider.as_ref(),
cx,
)
.log_err()?;
Some(TaskContext {
cwd,
task_variables,
})
}
task_context_impl(workspace, cx).unwrap_or_default()
}
fn combine_task_variables(
worktree_abs_path: Option<&Path>,
location: Location,
context_provider: &dyn ContextProvider,
cx: &mut WindowContext<'_>,
) -> anyhow::Result<TaskVariables> {
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)
.context("building provider context ")?,
);
Ok(basic_context)
}
}

View File

@@ -13,7 +13,7 @@ use crate::{
SyntaxLayer, SyntaxMap, SyntaxMapCapture, SyntaxMapCaptures, SyntaxMapMatches,
SyntaxSnapshot, ToTreeSitterPoint,
},
LanguageScope, Outline,
LanguageScope, Outline, RunnableTag,
};
use anyhow::{anyhow, Context, Result};
pub use clock::ReplicaId;
@@ -498,6 +498,13 @@ pub enum CharKind {
Word,
}
/// A runnable is a set of data about a region that could be resolved into a task
pub struct Runnable {
pub tags: SmallVec<[RunnableTag; 1]>,
pub language: Arc<Language>,
pub buffer: BufferId,
}
impl Buffer {
/// Create a new buffer with the given base text.
pub fn local<T: Into<String>>(base_text: T, cx: &mut ModelContext<Self>) -> Self {
@@ -2948,6 +2955,53 @@ impl BufferSnapshot {
})
}
pub fn runnable_ranges(
&self,
range: Range<Anchor>,
) -> impl Iterator<Item = (Range<usize>, Runnable)> + '_ {
let offset_range = range.start.to_offset(self)..range.end.to_offset(self);
let mut syntax_matches = self.syntax.matches(offset_range, self, |grammar| {
grammar.runnable_config.as_ref().map(|config| &config.query)
});
let test_configs = syntax_matches
.grammars()
.iter()
.map(|grammar| grammar.runnable_config.as_ref())
.collect::<Vec<_>>();
iter::from_fn(move || {
let test_range = syntax_matches
.peek()
.and_then(|mat| {
test_configs[mat.grammar_index].and_then(|test_configs| {
let tags = SmallVec::from_iter(mat.captures.iter().filter_map(|capture| {
test_configs.runnable_tags.get(&capture.index).cloned()
}));
if tags.is_empty() {
return None;
}
Some((
mat.captures
.iter()
.find(|capture| capture.index == test_configs.run_capture_ix)?,
Runnable {
tags,
language: mat.language,
buffer: self.remote_id(),
},
))
})
})
.map(|(mat, test_tags)| (mat.node.byte_range(), test_tags));
syntax_matches.advance();
test_range
})
}
/// Returns selections for remote peers intersecting the given range.
#[allow(clippy::type_complexity)]
pub fn remote_selections_in_range(

View File

@@ -56,6 +56,7 @@ use std::{
},
};
use syntax_map::SyntaxSnapshot;
use task::RunnableTag;
pub use task_context::{BasicContextProvider, ContextProvider, ContextProviderWithTasks};
use theme::SyntaxTheme;
use tree_sitter::{self, wasmtime, Query, WasmStore};
@@ -828,6 +829,7 @@ pub struct Grammar {
pub(crate) highlights_query: Option<Query>,
pub(crate) brackets_config: Option<BracketConfig>,
pub(crate) redactions_config: Option<RedactionConfig>,
pub(crate) runnable_config: Option<RunnableConfig>,
pub(crate) indents_config: Option<IndentConfig>,
pub outline_config: Option<OutlineConfig>,
pub embedding_config: Option<EmbeddingConfig>,
@@ -874,6 +876,14 @@ struct RedactionConfig {
pub redaction_capture_ix: u32,
}
struct RunnableConfig {
pub query: Query,
/// A mapping from captures indices to known test tags
pub runnable_tags: HashMap<u32, RunnableTag>,
/// index of the capture that corresponds to @run
pub run_capture_ix: u32,
}
struct OverrideConfig {
query: Query,
values: HashMap<u32, (String, LanguageConfigOverride)>,
@@ -915,6 +925,7 @@ impl Language {
injection_config: None,
override_config: None,
redactions_config: None,
runnable_config: None,
error_query: Query::new(&ts_language, "(ERROR) @error").unwrap(),
ts_language,
highlight_map: Default::default(),
@@ -970,6 +981,11 @@ impl Language {
.with_redaction_query(query.as_ref())
.context("Error loading redaction query")?;
}
if let Some(query) = queries.runnables {
self = self
.with_runnable_query(query.as_ref())
.context("Error loading tests query")?;
}
Ok(self)
}
@@ -981,6 +997,33 @@ impl Language {
Ok(self)
}
pub fn with_runnable_query(mut self, source: &str) -> Result<Self> {
let grammar = self
.grammar_mut()
.ok_or_else(|| anyhow!("cannot mutate grammar"))?;
let query = Query::new(&grammar.ts_language, source)?;
let mut run_capture_index = None;
let mut runnable_tags = HashMap::default();
for (ix, name) in query.capture_names().iter().enumerate() {
if *name == "run" {
run_capture_index = Some(ix as u32);
} else if !name.starts_with('_') {
runnable_tags.insert(ix as u32, RunnableTag(name.to_string().into()));
}
}
if let Some(run_capture_ix) = run_capture_index {
grammar.runnable_config = Some(RunnableConfig {
query,
run_capture_ix,
runnable_tags,
});
}
Ok(self)
}
pub fn with_outline_query(mut self, source: &str) -> Result<Self> {
let grammar = self
.grammar_mut()

View File

@@ -124,6 +124,7 @@ pub const QUERY_FILENAME_PREFIXES: &[(
("injections", |q| &mut q.injections),
("overrides", |q| &mut q.overrides),
("redactions", |q| &mut q.redactions),
("runnables", |q| &mut q.runnables),
];
/// Tree-sitter language queries for a given language.
@@ -137,6 +138,7 @@ pub struct LanguageQueries {
pub injections: Option<Cow<'static, str>>,
pub overrides: Option<Cow<'static, str>>,
pub redactions: Option<Cow<'static, str>>,
pub runnables: Option<Cow<'static, str>>,
}
#[derive(Clone, Default)]

View File

@@ -59,6 +59,7 @@ pub struct SyntaxMapCapture<'a> {
#[derive(Debug)]
pub struct SyntaxMapMatch<'a> {
pub language: Arc<Language>,
pub depth: usize,
pub pattern_index: usize,
pub captures: &'a [QueryCapture<'a>],
@@ -74,6 +75,7 @@ struct SyntaxMapCapturesLayer<'a> {
}
struct SyntaxMapMatchesLayer<'a> {
language: Arc<Language>,
depth: usize,
next_pattern_index: usize,
next_captures: Vec<QueryCapture<'a>>,
@@ -1004,6 +1006,7 @@ impl<'a> SyntaxMapMatches<'a> {
result.grammars.len() - 1
});
let mut layer = SyntaxMapMatchesLayer {
language: layer.language.clone(),
depth: layer.depth,
grammar_index,
matches,
@@ -1036,10 +1039,13 @@ impl<'a> SyntaxMapMatches<'a> {
pub fn peek(&self) -> Option<SyntaxMapMatch> {
let layer = self.layers.first()?;
if !layer.has_next {
return None;
}
Some(SyntaxMapMatch {
language: layer.language.clone(),
depth: layer.depth,
grammar_index: layer.grammar_index,
pattern_index: layer.next_pattern_index,

View File

@@ -389,6 +389,7 @@ impl ContextProvider for RustContextProvider {
"--".into(),
"--nocapture".into(),
],
tags: vec!["rust-test".to_owned()],
..TaskTemplate::default()
},
TaskTemplate {

View File

@@ -0,0 +1,7 @@
(
(attribute_item (attribute) @_attribute
(#match? @_attribute ".*test.*"))
.
(function_item
name: (_) @run)
) @rust-test

View File

@@ -13,7 +13,7 @@ use language::{
language_settings::{language_settings, LanguageSettings},
AutoindentMode, Buffer, BufferChunks, BufferSnapshot, Capability, CharKind, Chunk, CursorShape,
DiagnosticEntry, File, IndentSize, Language, LanguageScope, OffsetRangeExt, OffsetUtf16,
Outline, OutlineItem, Point, PointUtf16, Selection, TextDimension, ToOffset as _,
Outline, OutlineItem, Point, PointUtf16, Runnable, Selection, TextDimension, ToOffset as _,
ToOffsetUtf16 as _, ToPoint as _, ToPointUtf16 as _, TransactionId, Unclipped,
};
use smallvec::SmallVec;
@@ -3132,6 +3132,31 @@ impl MultiBufferSnapshot {
.flatten()
}
pub fn runnable_ranges(
&self,
range: Range<Anchor>,
) -> impl Iterator<Item = (Range<usize>, Runnable)> + '_ {
let range = range.start.to_offset(self)..range.end.to_offset(self);
self.excerpts_for_range(range.clone())
.flat_map(move |(excerpt, excerpt_offset)| {
let excerpt_buffer_start = excerpt.range.context.start.to_offset(&excerpt.buffer);
excerpt
.buffer
.runnable_ranges(excerpt.range.context.clone())
.map(move |(mut match_range, runnable)| {
// Re-base onto the excerpts coordinates in the multibuffer
match_range.start =
excerpt_offset + (match_range.start - excerpt_buffer_start);
match_range.end = excerpt_offset + (match_range.end - excerpt_buffer_start);
(match_range, runnable)
})
.skip_while(move |(match_range, _)| match_range.end < range.start)
.take_while(move |(match_range, _)| match_range.start < range.end)
})
}
pub fn diagnostics_update_count(&self) -> usize {
self.diagnostics_update_count
}

View File

@@ -7596,17 +7596,15 @@ impl Project {
} else {
let fs = self.fs.clone();
let task_abs_path = abs_path.clone();
let tasks_file_rx =
watch_config_file(&cx.background_executor(), fs, task_abs_path);
task_inventory.add_source(
TaskSourceKind::Worktree {
id: remote_worktree_id,
abs_path,
id_base: "local_tasks_for_worktree",
},
|cx| {
let tasks_file_rx =
watch_config_file(&cx.background_executor(), fs, task_abs_path);
StaticSource::new(TrackedFile::new(tasks_file_rx, cx), cx)
},
StaticSource::new(TrackedFile::new(tasks_file_rx, cx)),
cx,
);
}
@@ -7618,23 +7616,20 @@ impl Project {
} else {
let fs = self.fs.clone();
let task_abs_path = abs_path.clone();
let tasks_file_rx =
watch_config_file(&cx.background_executor(), fs, task_abs_path);
task_inventory.add_source(
TaskSourceKind::Worktree {
id: remote_worktree_id,
abs_path,
id_base: "local_vscode_tasks_for_worktree",
},
|cx| {
let tasks_file_rx =
watch_config_file(&cx.background_executor(), fs, task_abs_path);
StaticSource::new(
TrackedFile::new_convertible::<task::VsCodeTaskFile>(
tasks_file_rx,
cx,
),
StaticSource::new(
TrackedFile::new_convertible::<task::VsCodeTaskFile>(
tasks_file_rx,
cx,
)
},
),
),
cx,
);
}

View File

@@ -14,7 +14,7 @@ use serde_json::json;
#[cfg(not(windows))]
use std::os;
use std::task::Poll;
use task::{TaskContext, TaskSource, TaskTemplate, TaskTemplates};
use task::{TaskContext, TaskTemplate, TaskTemplates};
use unindent::Unindent as _;
use util::{assert_set_eq, paths::PathMatcher, test::temp_tree};
use worktree::WorktreeModelHandle as _;
@@ -168,12 +168,11 @@ async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext)
let all_tasks = project
.update(cx, |project, cx| {
project.task_inventory().update(cx, |inventory, cx| {
project.task_inventory().update(cx, |inventory, _| {
let (mut old, new) = inventory.used_and_current_resolved_tasks(
None,
Some(workree_id),
&task_context,
cx,
);
old.extend(new);
old
@@ -215,13 +214,9 @@ async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext)
project.update(cx, |project, cx| {
let inventory = project.task_inventory();
inventory.update(cx, |inventory, cx| {
let (mut old, new) = inventory.used_and_current_resolved_tasks(
None,
Some(workree_id),
&task_context,
cx,
);
inventory.update(cx, |inventory, _| {
let (mut old, new) =
inventory.used_and_current_resolved_tasks(None, Some(workree_id), &task_context);
old.extend(new);
let (_, resolved_task) = old
.into_iter()
@@ -231,41 +226,39 @@ async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext)
})
});
let tasks = serde_json::to_string(&TaskTemplates(vec![TaskTemplate {
label: "cargo check".to_string(),
command: "cargo".to_string(),
args: vec![
"check".to_string(),
"--all".to_string(),
"--all-targets".to_string(),
],
env: HashMap::from_iter(Some((
"RUSTFLAGS".to_string(),
"-Zunstable-options".to_string(),
))),
..TaskTemplate::default()
}]))
.unwrap();
let (tx, rx) = futures::channel::mpsc::unbounded();
let templates = cx.update(|cx| TrackedFile::new(rx, cx));
tx.unbounded_send(tasks).unwrap();
let source = StaticSource::new(templates);
cx.run_until_parked();
cx.update(|cx| {
let all_tasks = project
.update(cx, |project, cx| {
project.task_inventory().update(cx, |inventory, cx| {
inventory.remove_local_static_source(Path::new("/the-root/.zed/tasks.json"));
inventory.add_source(
global_task_source_kind.clone(),
|cx| {
cx.new_model(|_| {
let source = TestTaskSource {
tasks: TaskTemplates(vec![TaskTemplate {
label: "cargo check".to_string(),
command: "cargo".to_string(),
args: vec![
"check".to_string(),
"--all".to_string(),
"--all-targets".to_string(),
],
env: HashMap::from_iter(Some((
"RUSTFLAGS".to_string(),
"-Zunstable-options".to_string(),
))),
..TaskTemplate::default()
}]),
};
Box::new(source) as Box<_>
})
},
cx,
);
inventory.add_source(global_task_source_kind.clone(), source, cx);
let (mut old, new) = inventory.used_and_current_resolved_tasks(
None,
Some(workree_id),
&task_context,
cx,
);
old.extend(new);
old
@@ -317,20 +310,6 @@ async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext)
});
}
struct TestTaskSource {
tasks: TaskTemplates,
}
impl TaskSource for TestTaskSource {
fn as_any(&mut self) -> &mut dyn std::any::Any {
self
}
fn tasks_to_schedule(&mut self, _: &mut ModelContext<Box<dyn TaskSource>>) -> TaskTemplates {
self.tasks.clone()
}
}
#[gpui::test]
async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) {
init_test(cx);

View File

@@ -1,17 +1,18 @@
//! Project-wide storage of the tasks available, capable of updating itself from the sources set.
use std::{
any::TypeId,
cmp::{self, Reverse},
path::{Path, PathBuf},
sync::Arc,
};
use collections::{hash_map, HashMap, VecDeque};
use gpui::{AppContext, Context, Model, ModelContext, Subscription};
use gpui::{AppContext, Context, Model, ModelContext};
use itertools::{Either, Itertools};
use language::Language;
use task::{ResolvedTask, TaskContext, TaskId, TaskSource, TaskTemplate, VariableName};
use task::{
static_source::StaticSource, ResolvedTask, TaskContext, TaskId, TaskTemplate, VariableName,
};
use util::{post_inc, NumericPrefixWithSuffix};
use worktree::WorktreeId;
@@ -22,14 +23,12 @@ pub struct Inventory {
}
struct SourceInInventory {
source: Model<Box<dyn TaskSource>>,
_subscription: Subscription,
type_id: TypeId,
source: StaticSource,
kind: TaskSourceKind,
}
/// Kind of a source the tasks are fetched from, used to display more source information in the UI.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum TaskSourceKind {
/// bash-like commands spawned by users, not associated with any path
UserInput,
@@ -95,7 +94,7 @@ impl Inventory {
pub fn add_source(
&mut self,
kind: TaskSourceKind,
create_source: impl FnOnce(&mut ModelContext<Self>) -> Model<Box<dyn TaskSource>>,
source: StaticSource,
cx: &mut ModelContext<Self>,
) {
let abs_path = kind.abs_path();
@@ -106,16 +105,7 @@ impl Inventory {
}
}
let source = create_source(cx);
let type_id = source.read(cx).type_id();
let source = SourceInInventory {
_subscription: cx.observe(&source, |_, _, cx| {
cx.notify();
}),
source,
type_id,
kind,
};
let source = SourceInInventory { source, kind };
self.sources.push(source);
cx.notify();
}
@@ -136,31 +126,12 @@ impl Inventory {
self.sources.retain(|s| s.kind.worktree() != Some(worktree));
}
pub fn source<T: TaskSource>(&self) -> Option<(Model<Box<dyn TaskSource>>, TaskSourceKind)> {
let target_type_id = std::any::TypeId::of::<T>();
self.sources.iter().find_map(
|SourceInInventory {
type_id,
source,
kind,
..
}| {
if &target_type_id == type_id {
Some((source.clone(), kind.clone()))
} else {
None
}
},
)
}
/// Pulls its task sources relevant to the worktree and the language given,
/// returns all task templates with their source kinds, in no specific order.
pub fn list_tasks(
&self,
language: Option<Arc<Language>>,
worktree: Option<WorktreeId>,
cx: &mut AppContext,
) -> Vec<(TaskSourceKind, TaskTemplate)> {
let task_source_kind = language.as_ref().map(|language| TaskSourceKind::Language {
name: language.name(),
@@ -180,7 +151,7 @@ impl Inventory {
.flat_map(|source| {
source
.source
.update(cx, |source, cx| source.tasks_to_schedule(cx))
.tasks_to_schedule()
.0
.into_iter()
.map(|task| (&source.kind, task))
@@ -199,7 +170,6 @@ impl Inventory {
language: Option<Arc<Language>>,
worktree: Option<WorktreeId>,
task_context: &TaskContext,
cx: &mut AppContext,
) -> (
Vec<(TaskSourceKind, ResolvedTask)>,
Vec<(TaskSourceKind, ResolvedTask)>,
@@ -246,7 +216,7 @@ impl Inventory {
.flat_map(|source| {
source
.source
.update(cx, |source, cx| source.tasks_to_schedule(cx))
.tasks_to_schedule()
.0
.into_iter()
.map(|task| (&source.kind, task))
@@ -387,9 +357,12 @@ fn task_variables_preference(task: &ResolvedTask) -> Reverse<usize> {
#[cfg(test)]
mod test_inventory {
use gpui::{AppContext, Context as _, Model, ModelContext, TestAppContext};
use gpui::{AppContext, Model, TestAppContext};
use itertools::Itertools;
use task::{TaskContext, TaskId, TaskSource, TaskTemplate, TaskTemplates};
use task::{
static_source::{StaticSource, TrackedFile},
TaskContext, TaskTemplate, TaskTemplates,
};
use worktree::WorktreeId;
use crate::Inventory;
@@ -398,55 +371,28 @@ mod test_inventory {
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TestTask {
id: task::TaskId,
name: String,
}
pub struct StaticTestSource {
pub tasks: Vec<TestTask>,
}
impl StaticTestSource {
pub(super) fn new(
task_names: impl IntoIterator<Item = String>,
cx: &mut AppContext,
) -> Model<Box<dyn TaskSource>> {
cx.new_model(|_| {
Box::new(Self {
tasks: task_names
.into_iter()
.enumerate()
.map(|(i, name)| TestTask {
id: TaskId(format!("task_{i}_{name}")),
name,
})
.collect(),
}) as Box<dyn TaskSource>
})
}
}
impl TaskSource for StaticTestSource {
fn tasks_to_schedule(
&mut self,
_cx: &mut ModelContext<Box<dyn TaskSource>>,
) -> TaskTemplates {
TaskTemplates(
self.tasks
.clone()
.into_iter()
.map(|task| TaskTemplate {
label: task.name,
command: "test command".to_string(),
..TaskTemplate::default()
})
.collect(),
)
}
fn as_any(&mut self) -> &mut dyn std::any::Any {
self
}
pub(super) fn static_test_source(
task_names: impl IntoIterator<Item = String>,
cx: &mut AppContext,
) -> StaticSource {
let tasks = TaskTemplates(
task_names
.into_iter()
.map(|name| TaskTemplate {
label: name,
command: "test command".to_owned(),
..TaskTemplate::default()
})
.collect(),
);
let (tx, rx) = futures::channel::mpsc::unbounded();
let file = TrackedFile::new(rx, cx);
tx.unbounded_send(serde_json::to_string(&tasks).unwrap())
.unwrap();
StaticSource::new(file)
}
pub(super) fn task_template_names(
@@ -454,9 +400,9 @@ mod test_inventory {
worktree: Option<WorktreeId>,
cx: &mut TestAppContext,
) -> Vec<String> {
inventory.update(cx, |inventory, cx| {
inventory.update(cx, |inventory, _| {
inventory
.list_tasks(None, worktree, cx)
.list_tasks(None, worktree)
.into_iter()
.map(|(_, task)| task.label)
.sorted()
@@ -469,13 +415,9 @@ mod test_inventory {
worktree: Option<WorktreeId>,
cx: &mut TestAppContext,
) -> Vec<String> {
inventory.update(cx, |inventory, cx| {
let (used, current) = inventory.used_and_current_resolved_tasks(
None,
worktree,
&TaskContext::default(),
cx,
);
inventory.update(cx, |inventory, _| {
let (used, current) =
inventory.used_and_current_resolved_tasks(None, worktree, &TaskContext::default());
used.into_iter()
.chain(current)
.map(|(_, task)| task.original_task().label.clone())
@@ -488,9 +430,9 @@ mod test_inventory {
task_name: &str,
cx: &mut TestAppContext,
) {
inventory.update(cx, |inventory, cx| {
inventory.update(cx, |inventory, _| {
let (task_source_kind, task) = inventory
.list_tasks(None, None, cx)
.list_tasks(None, None)
.into_iter()
.find(|(_, task)| task.label == task_name)
.unwrap_or_else(|| panic!("Failed to find task with name {task_name}"));
@@ -508,13 +450,9 @@ mod test_inventory {
worktree: Option<WorktreeId>,
cx: &mut TestAppContext,
) -> Vec<(TaskSourceKind, String)> {
inventory.update(cx, |inventory, cx| {
let (used, current) = inventory.used_and_current_resolved_tasks(
None,
worktree,
&TaskContext::default(),
cx,
);
inventory.update(cx, |inventory, _| {
let (used, current) =
inventory.used_and_current_resolved_tasks(None, worktree, &TaskContext::default());
let mut all = used;
all.extend(current);
all.into_iter()
@@ -549,27 +487,25 @@ mod tests {
inventory.update(cx, |inventory, cx| {
inventory.add_source(
TaskSourceKind::UserInput,
|cx| StaticTestSource::new(vec!["3_task".to_string()], cx),
static_test_source(vec!["3_task".to_string()], cx),
cx,
);
});
inventory.update(cx, |inventory, cx| {
inventory.add_source(
TaskSourceKind::UserInput,
|cx| {
StaticTestSource::new(
vec![
"1_task".to_string(),
"2_task".to_string(),
"1_a_task".to_string(),
],
cx,
)
},
static_test_source(
vec![
"1_task".to_string(),
"2_task".to_string(),
"1_a_task".to_string(),
],
cx,
),
cx,
);
});
cx.run_until_parked();
let expected_initial_state = [
"1_a_task".to_string(),
"1_task".to_string(),
@@ -622,12 +558,11 @@ mod tests {
inventory.update(cx, |inventory, cx| {
inventory.add_source(
TaskSourceKind::UserInput,
|cx| {
StaticTestSource::new(vec!["10_hello".to_string(), "11_hello".to_string()], cx)
},
static_test_source(vec!["10_hello".to_string(), "11_hello".to_string()], cx),
cx,
);
});
cx.run_until_parked();
let expected_updated_state = [
"10_hello".to_string(),
"11_hello".to_string(),
@@ -680,15 +615,11 @@ mod tests {
let worktree_path_1 = Path::new("worktree_path_1");
let worktree_2 = WorktreeId::from_usize(2);
let worktree_path_2 = Path::new("worktree_path_2");
inventory_with_statics.update(cx, |inventory, cx| {
inventory.add_source(
TaskSourceKind::UserInput,
|cx| {
StaticTestSource::new(
vec!["user_input".to_string(), common_name.to_string()],
cx,
)
},
static_test_source(vec!["user_input".to_string(), common_name.to_string()], cx),
cx,
);
inventory.add_source(
@@ -696,12 +627,10 @@ mod tests {
id_base: "test source",
abs_path: path_1.to_path_buf(),
},
|cx| {
StaticTestSource::new(
vec!["static_source_1".to_string(), common_name.to_string()],
cx,
)
},
static_test_source(
vec!["static_source_1".to_string(), common_name.to_string()],
cx,
),
cx,
);
inventory.add_source(
@@ -709,12 +638,10 @@ mod tests {
id_base: "test source",
abs_path: path_2.to_path_buf(),
},
|cx| {
StaticTestSource::new(
vec!["static_source_2".to_string(), common_name.to_string()],
cx,
)
},
static_test_source(
vec!["static_source_2".to_string(), common_name.to_string()],
cx,
),
cx,
);
inventory.add_source(
@@ -723,12 +650,7 @@ mod tests {
abs_path: worktree_path_1.to_path_buf(),
id_base: "test_source",
},
|cx| {
StaticTestSource::new(
vec!["worktree_1".to_string(), common_name.to_string()],
cx,
)
},
static_test_source(vec!["worktree_1".to_string(), common_name.to_string()], cx),
cx,
);
inventory.add_source(
@@ -737,16 +659,11 @@ mod tests {
abs_path: worktree_path_2.to_path_buf(),
id_base: "test_source",
},
|cx| {
StaticTestSource::new(
vec!["worktree_2".to_string(), common_name.to_string()],
cx,
)
},
static_test_source(vec!["worktree_2".to_string(), common_name.to_string()], cx),
cx,
);
});
cx.run_until_parked();
let worktree_independent_tasks = vec![
(
TaskSourceKind::AbsPath {

View File

@@ -14,6 +14,7 @@ collections.workspace = true
futures.workspace = true
gpui.workspace = true
hex.workspace = true
parking_lot.workspace = true
schemars.workspace = true
serde.workspace = true
serde_json_lenient.workspace = true

View File

@@ -6,9 +6,8 @@ mod task_template;
mod vscode_format;
use collections::{HashMap, HashSet};
use gpui::ModelContext;
use gpui::SharedString;
use serde::Serialize;
use std::any::Any;
use std::borrow::Cow;
use std::path::PathBuf;
@@ -103,6 +102,8 @@ pub enum VariableName {
Column,
/// Text from the latest selection.
SelectedText,
/// The symbol selected by the symbol tagging system, specifically the @run capture in a runnables.scm
RunnableSymbol,
/// Custom variable, provided by the plugin or other external source.
/// Will be printed with `ZED_` prefix to avoid potential conflicts with other variables.
Custom(Cow<'static, str>),
@@ -132,6 +133,7 @@ impl std::fmt::Display for VariableName {
Self::Row => write!(f, "{ZED_VARIABLE_NAME_PREFIX}ROW"),
Self::Column => write!(f, "{ZED_VARIABLE_NAME_PREFIX}COLUMN"),
Self::SelectedText => write!(f, "{ZED_VARIABLE_NAME_PREFIX}SELECTED_TEXT"),
Self::RunnableSymbol => write!(f, "{ZED_VARIABLE_NAME_PREFIX}RUNNABLE_SYMBOL"),
Self::Custom(s) => write!(f, "{ZED_VARIABLE_NAME_PREFIX}CUSTOM_{s}"),
}
}
@@ -169,13 +171,6 @@ pub struct TaskContext {
pub task_variables: TaskVariables,
}
/// [`Source`] produces tasks that can be scheduled.
///
/// Implementations of this trait could be e.g. [`StaticSource`] that parses tasks from a .json files and provides process templates to be spawned;
/// another one could be a language server providing lenses with tests or build server listing all targets for a given project.
pub trait TaskSource: Any {
/// A way to erase the type of the source, processing and storing them generically.
fn as_any(&mut self) -> &mut dyn Any;
/// Collects all tasks available for scheduling.
fn tasks_to_schedule(&mut self, cx: &mut ModelContext<Box<dyn TaskSource>>) -> TaskTemplates;
}
/// This is a new type representing a 'tag' on a 'runnable symbol', typically a test of main() function, found via treesitter.
#[derive(Clone, Debug)]
pub struct RunnableTag(pub SharedString);

View File

@@ -1,134 +1,110 @@
//! A source of tasks, based on a static configuration, deserialized from the tasks config file, and related infrastructure for tracking changes to the file.
use std::sync::Arc;
use futures::StreamExt;
use gpui::{AppContext, Context, Model, ModelContext, Subscription};
use gpui::AppContext;
use parking_lot::RwLock;
use serde::Deserialize;
use util::ResultExt;
use crate::{TaskSource, TaskTemplates};
use crate::TaskTemplates;
use futures::channel::mpsc::UnboundedReceiver;
/// The source of tasks defined in a tasks config file.
pub struct StaticSource {
tasks: TaskTemplates,
_templates: Model<TrackedFile<TaskTemplates>>,
_subscription: Subscription,
tasks: TrackedFile<TaskTemplates>,
}
/// A Wrapper around deserializable T that keeps track of its contents
/// via a provided channel. Once T value changes, the observers of [`TrackedFile`] are
/// notified.
pub struct TrackedFile<T> {
parsed_contents: T,
parsed_contents: Arc<RwLock<T>>,
}
impl<T: PartialEq + 'static> TrackedFile<T> {
impl<T: PartialEq + 'static + Sync> TrackedFile<T> {
/// Initializes new [`TrackedFile`] with a type that's deserializable.
pub fn new(mut tracker: UnboundedReceiver<String>, cx: &mut AppContext) -> Model<Self>
pub fn new(mut tracker: UnboundedReceiver<String>, cx: &mut AppContext) -> Self
where
T: for<'a> Deserialize<'a> + Default,
T: for<'a> Deserialize<'a> + Default + Send,
{
cx.new_model(move |cx| {
cx.spawn(|tracked_file, mut cx| async move {
while let Some(new_contents) = tracker.next().await {
if !new_contents.trim().is_empty() {
// String -> T (ZedTaskFormat)
// String -> U (VsCodeFormat) -> Into::into T
let Some(new_contents) =
serde_json_lenient::from_str(&new_contents).log_err()
else {
continue;
};
tracked_file.update(&mut cx, |tracked_file: &mut TrackedFile<T>, cx| {
if tracked_file.parsed_contents != new_contents {
tracked_file.parsed_contents = new_contents;
cx.notify();
let parsed_contents: Arc<RwLock<T>> = Arc::default();
cx.background_executor()
.spawn({
let parsed_contents = parsed_contents.clone();
async move {
while let Some(new_contents) = tracker.next().await {
if Arc::strong_count(&parsed_contents) == 1 {
// We're no longer being observed. Stop polling.
break;
}
if !new_contents.trim().is_empty() {
let Some(new_contents) =
serde_json_lenient::from_str::<T>(&new_contents).log_err()
else {
continue;
};
})?;
let mut contents = parsed_contents.write();
*contents = new_contents;
}
}
anyhow::Ok(())
}
anyhow::Ok(())
})
.detach_and_log_err(cx);
Self {
parsed_contents: Default::default(),
}
})
Self { parsed_contents }
}
/// Initializes new [`TrackedFile`] with a type that's convertible from another deserializable type.
pub fn new_convertible<U: for<'a> Deserialize<'a> + TryInto<T, Error = anyhow::Error>>(
mut tracker: UnboundedReceiver<String>,
cx: &mut AppContext,
) -> Model<Self>
) -> Self
where
T: Default,
T: Default + Send,
{
cx.new_model(move |cx| {
cx.spawn(|tracked_file, mut cx| async move {
while let Some(new_contents) = tracker.next().await {
if !new_contents.trim().is_empty() {
let Some(new_contents) =
serde_json_lenient::from_str::<U>(&new_contents).log_err()
else {
continue;
};
let Some(new_contents) = new_contents.try_into().log_err() else {
continue;
};
tracked_file.update(&mut cx, |tracked_file: &mut TrackedFile<T>, cx| {
if tracked_file.parsed_contents != new_contents {
tracked_file.parsed_contents = new_contents;
cx.notify();
let parsed_contents: Arc<RwLock<T>> = Arc::default();
cx.background_executor()
.spawn({
let parsed_contents = parsed_contents.clone();
async move {
while let Some(new_contents) = tracker.next().await {
if Arc::strong_count(&parsed_contents) == 1 {
// We're no longer being observed. Stop polling.
break;
}
if !new_contents.trim().is_empty() {
let Some(new_contents) =
serde_json_lenient::from_str::<U>(&new_contents).log_err()
else {
continue;
};
})?;
let Some(new_contents) = new_contents.try_into().log_err() else {
continue;
};
let mut contents = parsed_contents.write();
*contents = new_contents;
}
}
anyhow::Ok(())
}
anyhow::Ok(())
})
.detach_and_log_err(cx);
Self {
parsed_contents: Default::default(),
}
})
}
fn get(&self) -> &T {
&self.parsed_contents
Self {
parsed_contents: Default::default(),
}
}
}
impl StaticSource {
/// Initializes the static source, reacting on tasks config changes.
pub fn new(
templates: Model<TrackedFile<TaskTemplates>>,
cx: &mut AppContext,
) -> Model<Box<dyn TaskSource>> {
cx.new_model(|cx| {
let _subscription = cx.observe(
&templates,
move |source: &mut Box<(dyn TaskSource + 'static)>, new_templates, cx| {
if let Some(static_source) = source.as_any().downcast_mut::<Self>() {
static_source.tasks = new_templates.read(cx).get().clone();
cx.notify();
}
},
);
Box::new(Self {
tasks: TaskTemplates::default(),
_templates: templates,
_subscription,
})
})
}
}
impl TaskSource for StaticSource {
fn tasks_to_schedule(&mut self, _: &mut ModelContext<Box<dyn TaskSource>>) -> TaskTemplates {
self.tasks.clone()
}
fn as_any(&mut self) -> &mut dyn std::any::Any {
self
pub fn new(tasks: TrackedFile<TaskTemplates>) -> Self {
Self { tasks }
}
/// Returns current list of tasks
pub fn tasks_to_schedule(&self) -> TaskTemplates {
self.tasks.parsed_contents.read().clone()
}
}

View File

@@ -58,6 +58,10 @@ pub struct TaskTemplate {
/// * `never` — avoid changing current terminal pane focus, but still add/reuse the task's tab there
#[serde(default)]
pub reveal: RevealStrategy,
/// Represents the tags which this template attaches to. Adding this removes this task from other UI.
#[serde(default)]
pub tags: Vec<String>,
}
/// What to do with the terminal pane and tab, after the command was started.

View File

@@ -9,7 +9,6 @@ license = "GPL-3.0-or-later"
workspace = true
[dependencies]
anyhow.workspace = true
editor.workspace = true
file_icons.workspace = true
fuzzy.workspace = true

View File

@@ -1,18 +1,13 @@
use std::{
path::{Path, PathBuf},
sync::Arc,
};
use std::sync::Arc;
use ::settings::Settings;
use anyhow::Context;
use editor::Editor;
use editor::{tasks::task_context, Editor};
use gpui::{AppContext, ViewContext, WindowContext};
use language::{BasicContextProvider, ContextProvider, Language};
use language::Language;
use modal::TasksModal;
use project::{Location, TaskSourceKind, WorktreeId};
use task::{ResolvedTask, TaskContext, TaskTemplate, TaskVariables};
use util::ResultExt;
use workspace::Workspace;
use project::WorktreeId;
use workspace::tasks::schedule_task;
use workspace::{tasks::schedule_resolved_task, Workspace};
mod modal;
mod settings;
@@ -93,9 +88,9 @@ fn spawn_task_with_name(name: String, cx: &mut ViewContext<Workspace>) {
.update(&mut cx, |workspace, cx| {
let (worktree, language) = active_item_selection_properties(workspace, cx);
let tasks = workspace.project().update(cx, |project, cx| {
project.task_inventory().update(cx, |inventory, cx| {
inventory.list_tasks(language, worktree, cx)
})
project
.task_inventory()
.update(cx, |inventory, _| inventory.list_tasks(language, worktree))
});
let (task_source_kind, target_task) =
tasks.into_iter().find(|(_, task)| task.label == name)?;
@@ -148,168 +143,6 @@ fn active_item_selection_properties(
(worktree_id, language)
}
fn task_context(workspace: &Workspace, cx: &mut WindowContext<'_>) -> TaskContext {
fn task_context_impl(workspace: &Workspace, cx: &mut WindowContext<'_>) -> Option<TaskContext> {
let cwd = task_cwd(workspace, cx).log_err().flatten();
let editor = workspace
.active_item(cx)
.and_then(|item| item.act_as::<Editor>(cx))?;
let (selection, buffer, editor_snapshot) = editor.update(cx, |editor, cx| {
let selection = editor.selections.newest::<usize>(cx);
let (buffer, _, _) = editor
.buffer()
.read(cx)
.point_to_buffer_offset(selection.start, cx)?;
let snapshot = editor.snapshot(cx);
Some((selection, buffer, snapshot))
})?;
let language_context_provider = buffer
.read(cx)
.language()
.and_then(|language| language.context_provider())
.unwrap_or_else(|| Arc::new(BasicContextProvider));
let selection_range = selection.range();
let start = editor_snapshot
.display_snapshot
.buffer_snapshot
.anchor_after(selection_range.start)
.text_anchor;
let end = editor_snapshot
.display_snapshot
.buffer_snapshot
.anchor_after(selection_range.end)
.text_anchor;
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 location = Location {
buffer,
range: start..end,
};
let task_variables = combine_task_variables(
worktree_abs_path.as_deref(),
location,
language_context_provider.as_ref(),
cx,
)
.log_err()?;
Some(TaskContext {
cwd,
task_variables,
})
}
task_context_impl(workspace, cx).unwrap_or_default()
}
fn combine_task_variables(
worktree_abs_path: Option<&Path>,
location: Location,
context_provider: &dyn ContextProvider,
cx: &mut WindowContext<'_>,
) -> anyhow::Result<TaskVariables> {
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)
.context("building provider context ")?,
);
Ok(basic_context)
}
}
fn schedule_task(
workspace: &Workspace,
task_source_kind: TaskSourceKind,
task_to_resolve: &TaskTemplate,
task_cx: &TaskContext,
omit_history: bool,
cx: &mut ViewContext<'_, Workspace>,
) {
if let Some(spawn_in_terminal) =
task_to_resolve.resolve_task(&task_source_kind.to_id_base(), task_cx)
{
schedule_resolved_task(
workspace,
task_source_kind,
spawn_in_terminal,
omit_history,
cx,
);
}
}
fn schedule_resolved_task(
workspace: &Workspace,
task_source_kind: TaskSourceKind,
mut resolved_task: ResolvedTask,
omit_history: bool,
cx: &mut ViewContext<'_, Workspace>,
) {
if let Some(spawn_in_terminal) = resolved_task.resolved.take() {
if !omit_history {
resolved_task.resolved = Some(spawn_in_terminal.clone());
workspace.project().update(cx, |project, cx| {
project.task_inventory().update(cx, |inventory, _| {
inventory.task_scheduled(task_source_kind, resolved_task);
})
});
}
cx.emit(workspace::Event::SpawnTask(spawn_in_terminal));
}
}
fn task_cwd(workspace: &Workspace, cx: &mut WindowContext) -> anyhow::Result<Option<PathBuf>> {
let project = workspace.project().read(cx);
let available_worktrees = project
.worktrees()
.filter(|worktree| {
let worktree = worktree.read(cx);
worktree.is_visible()
&& worktree.is_local()
&& worktree.root_entry().map_or(false, |e| e.is_dir())
})
.collect::<Vec<_>>();
let cwd = match available_worktrees.len() {
0 => None,
1 => Some(available_worktrees[0].read(cx).abs_path()),
_ => {
let cwd_for_active_entry = project.active_entry().and_then(|entry_id| {
available_worktrees.into_iter().find_map(|worktree| {
let worktree = worktree.read(cx);
if worktree.contains_entry(entry_id) {
Some(worktree.abs_path())
} else {
None
}
})
});
anyhow::ensure!(
cwd_for_active_entry.is_some(),
"Cannot determine task cwd for multiple worktrees"
);
cwd_for_active_entry
}
};
Ok(cwd.map(|path| path.to_path_buf()))
}
#[cfg(test)]
mod tests {
use std::sync::Arc;

View File

@@ -1,6 +1,6 @@
use std::sync::Arc;
use crate::{active_item_selection_properties, schedule_resolved_task};
use crate::active_item_selection_properties;
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{
impl_actions, rems, AppContext, DismissEvent, EventEmitter, FocusableView, Global,
@@ -16,7 +16,7 @@ use ui::{
Tooltip, WindowContext,
};
use util::ResultExt;
use workspace::{ModalView, Workspace};
use workspace::{tasks::schedule_resolved_task, ModalView, Workspace};
use serde::Deserialize;
@@ -209,12 +209,11 @@ impl PickerDelegate for TasksModalDelegate {
return Vec::new();
};
let (used, current) =
picker.delegate.inventory.update(cx, |inventory, cx| {
picker.delegate.inventory.update(cx, |inventory, _| {
inventory.used_and_current_resolved_tasks(
language,
worktree,
&picker.delegate.task_context,
cx,
)
});
picker.delegate.last_used_candidate_index = if used.is_empty() {

View File

@@ -0,0 +1,83 @@
use std::path::PathBuf;
use project::TaskSourceKind;
use task::{ResolvedTask, TaskContext, TaskTemplate};
use ui::{ViewContext, WindowContext};
use crate::Workspace;
pub fn task_cwd(workspace: &Workspace, cx: &mut WindowContext) -> anyhow::Result<Option<PathBuf>> {
let project = workspace.project().read(cx);
let available_worktrees = project
.worktrees()
.filter(|worktree| {
let worktree = worktree.read(cx);
worktree.is_visible()
&& worktree.is_local()
&& worktree.root_entry().map_or(false, |e| e.is_dir())
})
.collect::<Vec<_>>();
let cwd = match available_worktrees.len() {
0 => None,
1 => Some(available_worktrees[0].read(cx).abs_path()),
_ => {
let cwd_for_active_entry = project.active_entry().and_then(|entry_id| {
available_worktrees.into_iter().find_map(|worktree| {
let worktree = worktree.read(cx);
if worktree.contains_entry(entry_id) {
Some(worktree.abs_path())
} else {
None
}
})
});
anyhow::ensure!(
cwd_for_active_entry.is_some(),
"Cannot determine task cwd for multiple worktrees"
);
cwd_for_active_entry
}
};
Ok(cwd.map(|path| path.to_path_buf()))
}
pub fn schedule_task(
workspace: &Workspace,
task_source_kind: TaskSourceKind,
task_to_resolve: &TaskTemplate,
task_cx: &TaskContext,
omit_history: bool,
cx: &mut ViewContext<'_, Workspace>,
) {
if let Some(spawn_in_terminal) =
task_to_resolve.resolve_task(&task_source_kind.to_id_base(), task_cx)
{
schedule_resolved_task(
workspace,
task_source_kind,
spawn_in_terminal,
omit_history,
cx,
);
}
}
pub fn schedule_resolved_task(
workspace: &Workspace,
task_source_kind: TaskSourceKind,
mut resolved_task: ResolvedTask,
omit_history: bool,
cx: &mut ViewContext<'_, Workspace>,
) {
if let Some(spawn_in_terminal) = resolved_task.resolved.take() {
if !omit_history {
resolved_task.resolved = Some(spawn_in_terminal.clone());
workspace.project().update(cx, |project, cx| {
project.task_inventory().update(cx, |inventory, _| {
inventory.task_scheduled(task_source_kind, resolved_task);
})
});
}
cx.emit(crate::Event::SpawnTask(spawn_in_terminal));
}
}

View File

@@ -8,6 +8,7 @@ mod persistence;
pub mod searchable;
pub mod shared_screen;
mod status_bar;
pub mod tasks;
mod toolbar;
mod workspace_settings;

View File

@@ -291,7 +291,7 @@ fn init_ui(args: Args) {
load_user_themes_in_background(fs.clone(), cx);
watch_themes(fs.clone(), cx);
watch_languages(fs.clone(), languages.clone(), cx);
watch_file_types(fs.clone(), cx);
languages.set_theme(cx.theme().clone());
@@ -1159,6 +1159,37 @@ fn watch_themes(fs: Arc<dyn fs::Fs>, cx: &mut AppContext) {
.detach()
}
#[cfg(debug_assertions)]
fn watch_languages(fs: Arc<dyn fs::Fs>, languages: Arc<LanguageRegistry>, cx: &mut AppContext) {
use std::time::Duration;
let path = {
let p = Path::new("crates/languages/src");
let Ok(full_path) = p.canonicalize() else {
return;
};
full_path
};
cx.spawn(|_| async move {
let mut events = fs.watch(path.as_path(), Duration::from_millis(100)).await;
while let Some(event) = events.next().await {
let has_language_file = event.iter().any(|path| {
path.extension()
.map(|ext| ext.to_string_lossy().as_ref() == "scm")
.unwrap_or(false)
});
if has_language_file {
languages.reload();
}
}
})
.detach()
}
#[cfg(not(debug_assertions))]
fn watch_languages(_fs: Arc<dyn fs::Fs>, _languages: Arc<LanguageRegistry>, _cx: &mut AppContext) {}
#[cfg(debug_assertions)]
fn watch_file_types(fs: Arc<dyn fs::Fs>, cx: &mut AppContext) {
use std::time::Duration;

View File

@@ -162,19 +162,14 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
project.update(cx, |project, cx| {
let fs = app_state.fs.clone();
project.task_inventory().update(cx, |inventory, cx| {
let tasks_file_rx =
watch_config_file(&cx.background_executor(), fs, paths::TASKS.clone());
inventory.add_source(
TaskSourceKind::AbsPath {
id_base: "global_tasks",
abs_path: paths::TASKS.clone(),
},
|cx| {
let tasks_file_rx = watch_config_file(
&cx.background_executor(),
fs,
paths::TASKS.clone(),
);
StaticSource::new(TrackedFile::new(tasks_file_rx, cx), cx)
},
StaticSource::new(TrackedFile::new(tasks_file_rx, cx)),
cx,
);
})