editor: Add Organize Imports Action (#25793)
Closes #10004 This PR adds support for the organize imports action. Previously, you had to manually configure it in the settings and then use format to run it. Note: Default key binding will be `alt-shift-o` which is similar to VSCode's organize import. Also, because `cmd-shift-o` is taken by outline picker. Todo: - [x] Initial working - [x] Handle remote - [x] Handle multi buffer - [x] Can we make it generic for executing any code action? Release Notes: - Added `editor:OrganizeImports` action to organize imports (sort, remove unused, etc) for supported LSPs. You can trigger it by using the `alt-shift-o` key binding.
This commit is contained in:
parent
e4e758db3a
commit
fad4df5e70
10 changed files with 408 additions and 4 deletions
|
@ -348,6 +348,7 @@ gpui::actions!(
|
|||
OpenPermalinkToLine,
|
||||
OpenSelectionsInMultibuffer,
|
||||
OpenUrl,
|
||||
OrganizeImports,
|
||||
Outdent,
|
||||
AutoIndent,
|
||||
PageDown,
|
||||
|
|
|
@ -120,8 +120,8 @@ use task::{ResolvedTask, TaskTemplate, TaskVariables};
|
|||
use hover_links::{find_file, HoverLink, HoveredLinkState, InlayHighlight};
|
||||
pub use lsp::CompletionContext;
|
||||
use lsp::{
|
||||
CompletionItemKind, CompletionTriggerKind, DiagnosticSeverity, InsertTextFormat,
|
||||
LanguageServerId, LanguageServerName,
|
||||
CodeActionKind, CompletionItemKind, CompletionTriggerKind, DiagnosticSeverity,
|
||||
InsertTextFormat, LanguageServerId, LanguageServerName,
|
||||
};
|
||||
|
||||
use language::BufferSnapshot;
|
||||
|
@ -203,6 +203,7 @@ pub(crate) const CURSORS_VISIBLE_FOR: Duration = Duration::from_millis(2000);
|
|||
#[doc(hidden)]
|
||||
pub const CODE_ACTIONS_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(250);
|
||||
|
||||
pub(crate) const CODE_ACTION_TIMEOUT: Duration = Duration::from_secs(5);
|
||||
pub(crate) const FORMAT_TIMEOUT: Duration = Duration::from_secs(5);
|
||||
pub(crate) const SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT: Duration = Duration::from_secs(1);
|
||||
|
||||
|
@ -12494,7 +12495,6 @@ impl Editor {
|
|||
buffer.push_transaction(&transaction.0, cx);
|
||||
}
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
|
@ -12503,6 +12503,60 @@ impl Editor {
|
|||
})
|
||||
}
|
||||
|
||||
fn organize_imports(
|
||||
&mut self,
|
||||
_: &OrganizeImports,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<Task<Result<()>>> {
|
||||
let project = match &self.project {
|
||||
Some(project) => project.clone(),
|
||||
None => return None,
|
||||
};
|
||||
Some(self.perform_code_action_kind(
|
||||
project,
|
||||
CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
|
||||
window,
|
||||
cx,
|
||||
))
|
||||
}
|
||||
|
||||
fn perform_code_action_kind(
|
||||
&mut self,
|
||||
project: Entity<Project>,
|
||||
kind: CodeActionKind,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
let buffer = self.buffer.clone();
|
||||
let buffers = buffer.read(cx).all_buffers();
|
||||
let mut timeout = cx.background_executor().timer(CODE_ACTION_TIMEOUT).fuse();
|
||||
let apply_action = project.update(cx, |project, cx| {
|
||||
project.apply_code_action_kind(buffers, kind, true, cx)
|
||||
});
|
||||
cx.spawn_in(window, |_, mut cx| async move {
|
||||
let transaction = futures::select_biased! {
|
||||
() = timeout => {
|
||||
log::warn!("timed out waiting for executing code action");
|
||||
None
|
||||
}
|
||||
transaction = apply_action.log_err().fuse() => transaction,
|
||||
};
|
||||
buffer
|
||||
.update(&mut cx, |buffer, cx| {
|
||||
// check if we need this
|
||||
if let Some(transaction) = transaction {
|
||||
if !buffer.is_singleton() {
|
||||
buffer.push_transaction(&transaction.0, cx);
|
||||
}
|
||||
}
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn restart_language_server(
|
||||
&mut self,
|
||||
_: &RestartLanguageServer,
|
||||
|
|
|
@ -7875,6 +7875,157 @@ async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
|
|||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
|
||||
init_test(cx, |settings| {
|
||||
settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
|
||||
FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
|
||||
))
|
||||
});
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_file(path!("/file.ts"), Default::default()).await;
|
||||
|
||||
let project = Project::test(fs, [path!("/").as_ref()], cx).await;
|
||||
|
||||
let language_registry = project.read_with(cx, |project, _| project.languages().clone());
|
||||
language_registry.add(Arc::new(Language::new(
|
||||
LanguageConfig {
|
||||
name: "TypeScript".into(),
|
||||
matcher: LanguageMatcher {
|
||||
path_suffixes: vec!["ts".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
..LanguageConfig::default()
|
||||
},
|
||||
Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
|
||||
)));
|
||||
update_test_language_settings(cx, |settings| {
|
||||
settings.defaults.prettier = Some(PrettierSettings {
|
||||
allowed: true,
|
||||
..PrettierSettings::default()
|
||||
});
|
||||
});
|
||||
let mut fake_servers = language_registry.register_fake_lsp(
|
||||
"TypeScript",
|
||||
FakeLspAdapter {
|
||||
capabilities: lsp::ServerCapabilities {
|
||||
code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_local_buffer(path!("/file.ts"), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
let (editor, cx) = cx.add_window_view(|window, cx| {
|
||||
build_editor_with_project(project.clone(), buffer, window, cx)
|
||||
});
|
||||
editor.update_in(cx, |editor, window, cx| {
|
||||
editor.set_text(
|
||||
"import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
cx.executor().start_waiting();
|
||||
let fake_server = fake_servers.next().await.unwrap();
|
||||
|
||||
let format = editor
|
||||
.update_in(cx, |editor, window, cx| {
|
||||
editor.perform_code_action_kind(
|
||||
project.clone(),
|
||||
CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.unwrap();
|
||||
fake_server
|
||||
.handle_request::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
|
||||
assert_eq!(
|
||||
params.text_document.uri,
|
||||
lsp::Url::from_file_path(path!("/file.ts")).unwrap()
|
||||
);
|
||||
Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
|
||||
lsp::CodeAction {
|
||||
title: "Organize Imports".to_string(),
|
||||
kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
|
||||
edit: Some(lsp::WorkspaceEdit {
|
||||
changes: Some(
|
||||
[(
|
||||
params.text_document.uri.clone(),
|
||||
vec![lsp::TextEdit::new(
|
||||
lsp::Range::new(
|
||||
lsp::Position::new(1, 0),
|
||||
lsp::Position::new(2, 0),
|
||||
),
|
||||
"".to_string(),
|
||||
)],
|
||||
)]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
)]))
|
||||
})
|
||||
.next()
|
||||
.await;
|
||||
cx.executor().start_waiting();
|
||||
format.await;
|
||||
assert_eq!(
|
||||
editor.update(cx, |editor, cx| editor.text(cx)),
|
||||
"import { a } from 'module';\n\nconst x = a;\n"
|
||||
);
|
||||
|
||||
editor.update_in(cx, |editor, window, cx| {
|
||||
editor.set_text(
|
||||
"import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
// Ensure we don't lock if code action hangs.
|
||||
fake_server.handle_request::<lsp::request::CodeActionRequest, _, _>(
|
||||
move |params, _| async move {
|
||||
assert_eq!(
|
||||
params.text_document.uri,
|
||||
lsp::Url::from_file_path(path!("/file.ts")).unwrap()
|
||||
);
|
||||
futures::future::pending::<()>().await;
|
||||
unreachable!()
|
||||
},
|
||||
);
|
||||
let format = editor
|
||||
.update_in(cx, |editor, window, cx| {
|
||||
editor.perform_code_action_kind(
|
||||
project,
|
||||
CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.unwrap();
|
||||
cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
|
||||
cx.executor().start_waiting();
|
||||
format.await;
|
||||
assert_eq!(
|
||||
editor.update(cx, |editor, cx| editor.text(cx)),
|
||||
"import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
|
|
@ -429,6 +429,13 @@ impl EditorElement {
|
|||
cx.propagate();
|
||||
}
|
||||
});
|
||||
register_action(editor, window, |editor, action, window, cx| {
|
||||
if let Some(task) = editor.organize_imports(action, window, cx) {
|
||||
task.detach_and_notify_err(window, cx);
|
||||
} else {
|
||||
cx.propagate();
|
||||
}
|
||||
});
|
||||
register_action(editor, window, Editor::restart_language_server);
|
||||
register_action(editor, window, Editor::show_character_palette);
|
||||
register_action(editor, window, |editor, action, window, cx| {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue