дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 11:15:40 +02:00
Project & files - Project view modes (Project Files / Open Files / All Changed Files / Scratches); External Libraries for Python, JDK (src.zip), Node, Cargo, Go, Maven/Gradle jars; read-only jar/zip browsing - ⌘N New popup with type search and more file kinds; New Project, Rename Project, Reload All from Disk, Export/Import Settings, Power Save Mode - File type associations, extra content roots and exclude patterns (indexed and watched without reopening the project) Editor & navigation - Find bar: search scope (comments/strings), Preserve Case, Exclude, multiline, history; Find in Files: Module scope, custom pattern scopes, multiline - Search Everywhere filters, Assign Shortcut, settings results; named bookmark lists, file/folder bookmarks; Structure filters and Show Inherited; Recent Locations timestamps and breadcrumbs; Code Vision author - Completion options, editor tab placement/layout, inline completion (LSP 3.18) - Spell checking (Typo inspection, ide-spell) and Inspections settings with custom profiles; Code Style schemes, formatter markers, Do not format Run, VCS, services, database - Typed run configurations (Shell, Program, npm, Gradle, Maven, Docker, Compound), Modify options, Before launch, env dialog, Current File, Emulate terminal (PTY) - Git log graph/view options, Go to Hash/Branch/Tag, date filter keeps the graph; Pull/Merge/Rebase dialogs, Manage Remotes, Worktrees - Generic Services host (Docker connections/registries, run sessions); TODO scope/changes tabs with changelists; Coverage report import - Data sources: wire-compatible vendors, URL import, SSL certs, Options, Schemas and Advanced tabs, color and comment Workbench - Run Anything, Customize Main Toolbar, named tool window layouts, menu completeness, Code Cleanup, macros, "Don't Show Again", editor banners - Chinese translations for the new UI Claude-Session: https://claude.ai/code/session_01SaPPsVPuk6ZTNsoUCNCbMv
203 рядки
7.2 KiB
Rust
203 рядки
7.2 KiB
Rust
use crate::state::AppState;
|
||
use ide_core::{IdeResult, Pos, RequestId, TextEdit};
|
||
use ide_search::{
|
||
FileMatches, ProjectFileSet, ReplaceEvent, ReplaceHost, ReplacePreview, ReplaceTarget, SearchQuery, SearchSummary,
|
||
};
|
||
use ide_workspace::Documents;
|
||
use serde::{Deserialize, Serialize};
|
||
use specta::Type;
|
||
use std::path::{Path, PathBuf};
|
||
use tauri::ipc::Channel;
|
||
use tauri::{State, Window};
|
||
|
||
#[derive(Clone, Serialize, Deserialize, Type)]
|
||
#[serde(tag = "type", rename_all = "camelCase")]
|
||
pub enum SearchEvent {
|
||
File { file: FileMatches },
|
||
Done { summary: SearchSummary },
|
||
}
|
||
|
||
/// Find in Files. Results stream through `on_event`; ends with `Done`.
|
||
/// Cancel with `cancel_request(request)`.
|
||
#[tauri::command]
|
||
#[specta::specta]
|
||
pub async fn search_start(
|
||
window: Window,
|
||
state: State<'_, AppState>,
|
||
request: RequestId,
|
||
mut query: SearchQuery,
|
||
on_event: Channel<SearchEvent>,
|
||
) -> IdeResult<()> {
|
||
let project = state.project(window.label())?;
|
||
// Excluded roots (Project Structure marks and detected `target`, `node_modules`, …) and
|
||
// ignored names are never searched.
|
||
query.exclude.extend(project.search_excludes());
|
||
let roots = project.roots();
|
||
let set = match query.scope {
|
||
ide_search::SearchScope::FileSet { set } => Some(set),
|
||
_ => None,
|
||
};
|
||
let cancel = state.begin_request(request);
|
||
let documents = project.documents.clone();
|
||
let root = project.root.clone();
|
||
// The whole project also covers the Project Structure content roots.
|
||
let content_roots = match query.scope {
|
||
ide_search::SearchScope::Project | ide_search::SearchScope::FileSet { set: ProjectFileSet::All } => {
|
||
project.content_roots()
|
||
}
|
||
_ => Vec::new(),
|
||
};
|
||
let languages = project.languages.clone();
|
||
let syntax = state.syntax();
|
||
let events = on_event.clone();
|
||
let summary = tauri::async_runtime::spawn_blocking(move || {
|
||
let filter = |rel: &str| match set {
|
||
Some(ProjectFileSet::Production) => !roots.is_test(rel),
|
||
Some(ProjectFileSet::Test) => roots.is_test(rel),
|
||
Some(ProjectFileSet::All) | None => true,
|
||
};
|
||
// Comments and string literals for the search context filter.
|
||
let literals = |path: &Path, text: &str| {
|
||
let language = languages.detect(path, text.lines().next());
|
||
let found = syntax.literals(&language, text)?;
|
||
Some(found.into_iter().map(literal_range).collect())
|
||
};
|
||
let mut total = ide_search::SearchSummary::default();
|
||
for dir in std::iter::once(&root).chain(&content_roots) {
|
||
let s = ide_search::search_in_context(
|
||
dir,
|
||
&query,
|
||
Some(&filter),
|
||
Some(&literals),
|
||
&|p| documents.text_of(p),
|
||
&cancel,
|
||
&|file| {
|
||
let _ = events.send(SearchEvent::File { file });
|
||
},
|
||
)?;
|
||
total.files += s.files;
|
||
total.matches += s.matches;
|
||
total.elapsed_ms += s.elapsed_ms;
|
||
total.limit_hit |= s.limit_hit;
|
||
total.cancelled |= s.cancelled;
|
||
if total.limit_hit || total.cancelled {
|
||
break;
|
||
}
|
||
}
|
||
Ok::<_, ide_core::IdeError>(total)
|
||
})
|
||
.await
|
||
.map_err(|e| ide_core::IdeError::internal(e.to_string()))?;
|
||
state.end_request(request);
|
||
let _ = on_event.send(SearchEvent::Done { summary: summary? });
|
||
Ok(())
|
||
}
|
||
|
||
/// Replace in Files. Runs off the command thread; progress streams through
|
||
/// `on_event` and ends with `Done`. Cancel with `cancel_request(request)`.
|
||
///
|
||
/// Edits go through the document layer as one patch per file, so open editors
|
||
/// update live and undo the whole file's replacements in one step. Unsaved
|
||
/// editor buffers are replaced in place and left unsaved; files that were not
|
||
/// open are opened transiently, saved and closed.
|
||
#[tauri::command]
|
||
#[specta::specta]
|
||
#[allow(clippy::too_many_arguments)]
|
||
pub async fn replace_in_files(
|
||
window: Window,
|
||
state: State<'_, AppState>,
|
||
request: RequestId,
|
||
query: SearchQuery,
|
||
replacement: String,
|
||
preserve_case: bool,
|
||
targets: Vec<ReplaceTarget>,
|
||
on_event: Channel<ReplaceEvent>,
|
||
) -> IdeResult<()> {
|
||
let project = state.project(window.label())?;
|
||
let cancel = state.begin_request(request);
|
||
let events = on_event.clone();
|
||
let summary = tauri::async_runtime::spawn_blocking(move || {
|
||
let host = DocumentsHost(&project.documents);
|
||
ide_search::replace_files(&host, &query, &replacement, preserve_case, &targets, &cancel, &|e| {
|
||
let _ = events.send(e);
|
||
})
|
||
})
|
||
.await
|
||
.map_err(|e| ide_core::IdeError::internal(e.to_string()));
|
||
state.end_request(request);
|
||
let summary = summary??;
|
||
let _ = on_event.send(ReplaceEvent::Done { summary });
|
||
Ok(())
|
||
}
|
||
|
||
/// Before / after text of `path` for Replace › Preview (unsaved buffer included).
|
||
#[tauri::command]
|
||
#[specta::specta]
|
||
pub async fn replace_preview(
|
||
window: Window,
|
||
state: State<'_, AppState>,
|
||
query: SearchQuery,
|
||
replacement: String,
|
||
preserve_case: bool,
|
||
path: PathBuf,
|
||
only: Option<Vec<Pos>>,
|
||
) -> IdeResult<ReplacePreview> {
|
||
let project = state.project(window.label())?;
|
||
tauri::async_runtime::spawn_blocking(move || {
|
||
let text = project.text_of(&path)?;
|
||
ide_search::preview(&text, &query, &replacement, preserve_case, only.as_deref())
|
||
})
|
||
.await
|
||
.map_err(|e| ide_core::IdeError::internal(e.to_string()))?
|
||
}
|
||
|
||
fn literal_range(l: ide_language::Literal) -> ide_search::LiteralRange {
|
||
let kind = match l.kind {
|
||
ide_language::LiteralKind::Comment => ide_search::LiteralKind::Comment,
|
||
ide_language::LiteralKind::String => ide_search::LiteralKind::String,
|
||
};
|
||
ide_search::LiteralRange { start: l.start, end: l.end, kind }
|
||
}
|
||
|
||
struct DocumentsHost<'a>(&'a Documents);
|
||
|
||
impl ReplaceHost for DocumentsHost<'_> {
|
||
fn edit_file(&self, path: &Path, compute: &mut dyn FnMut(&str) -> IdeResult<Vec<TextEdit>>) -> IdeResult<u32> {
|
||
let docs = self.0;
|
||
let (doc, transient) = match docs.by_path(path) {
|
||
Some(doc) => (doc, false),
|
||
None => (docs.open(path)?.id, true),
|
||
};
|
||
let result = (|| {
|
||
// Recompute if the user typed between reading the text and applying.
|
||
for _ in 0..3 {
|
||
let snap = docs.text_snapshot(doc)?;
|
||
let edits = compute(&snap.text)?;
|
||
if edits.is_empty() {
|
||
return Ok(0);
|
||
}
|
||
if docs.text_snapshot(doc)?.version != snap.version {
|
||
continue;
|
||
}
|
||
docs.apply_edits(doc, &edits)?;
|
||
if transient {
|
||
docs.save(doc)?;
|
||
}
|
||
return Ok(edits.len() as u32);
|
||
}
|
||
Err(ide_core::IdeError::unavailable("the file kept changing during Replace"))
|
||
})();
|
||
if transient {
|
||
docs.close(doc);
|
||
}
|
||
result
|
||
}
|
||
}
|
||
|
||
/// Validates a query (e.g. regex syntax) while the user types.
|
||
#[tauri::command]
|
||
#[specta::specta]
|
||
pub fn search_validate(query: SearchQuery) -> IdeResult<()> {
|
||
ide_search::validate_query(&query)
|
||
}
|