diff --git a/rust/lithe-core/src/git.rs b/rust/lithe-core/src/git.rs index 6ed41a26f..bc1191d5c 100644 --- a/rust/lithe-core/src/git.rs +++ b/rust/lithe-core/src/git.rs @@ -10,6 +10,8 @@ use crate::model::{ use serde::{Deserialize, Serialize}; use std::io::Read; use std::io::Write; +#[cfg(windows)] +use std::os::windows::process::CommandExt; use std::path::{Path, PathBuf}; use std::process::Command; use std::thread; @@ -477,7 +479,7 @@ fn execute_git( input: Option, ) -> Result { crate::cancellation::check()?; - let mut process = Command::new("git"); + let mut process = git_command(); process.args(arguments).current_dir(root); process.stdin(if input.is_some() { std::process::Stdio::piped() @@ -2624,7 +2626,7 @@ pub fn status(request: GitStatusRequest) -> Result } fn run_git(directory: &Path, arguments: &[&str]) -> Result { - Command::new("git") + git_command() .args(arguments) .current_dir(directory) .output() @@ -2634,6 +2636,13 @@ fn run_git(directory: &Path, arguments: &[&str]) -> Result Command { + let mut command = Command::new("git"); + #[cfg(windows)] + command.creation_flags(0x0800_0000); + command +} + fn parse_status(output: &[u8]) -> Vec { let mut changes = Vec::new(); let records = output diff --git a/windows/winui/Lithe.WinUI.vcxproj b/windows/winui/Lithe.WinUI.vcxproj index 25cb854de..2089b2690 100644 --- a/windows/winui/Lithe.WinUI.vcxproj +++ b/windows/winui/Lithe.WinUI.vcxproj @@ -16,6 +16,7 @@ target even though the application consumes the UAP Windows SDK. --> native,Version=v0.0 win10-x64;win10-arm64 + 10.0.26100.0 10.0.26100.0 10.0.17763.0 Windows Store diff --git a/windows/winui/MainWindow.xaml b/windows/winui/MainWindow.xaml index 8efaede75..5648d5211 100644 --- a/windows/winui/MainWindow.xaml +++ b/windows/winui/MainWindow.xaml @@ -263,6 +263,8 @@ Grid.Row="1" SelectionMode="Single" ItemInvoked="ProjectItemInvoked" + Expanding="ProjectTreeExpanding" + Collapsed="ProjectTreeCollapsed" RightTapped="ProjectTreeRightTapped" Padding="4,0,4,8"> @@ -505,10 +507,11 @@ - (); } -Windows::UI::Color applicationColor(wchar_t const* key) { - return unbox_value( - Application::Current().Resources().Lookup(box_value(hstring(key)))); +Windows::UI::Color applicationColor(wchar_t const* key, ElementTheme theme) { + const auto themeKey = theme == ElementTheme::Dark ? L"Dark" : L"Default"; + const auto dictionary = Application::Current().Resources().ThemeDictionaries() + .Lookup(box_value(themeKey)).as(); + return unbox_value(dictionary.Lookup(box_value(hstring(key)))); } TextBlock diffCell(std::string value, int column, bool muted = false) { @@ -1179,15 +1181,21 @@ void MainWindow::configureTimers() { debugPollTimer_.Interval(std::chrono::milliseconds(100)); debugPollTimer_.IsRepeating(true); debugPollToken_ = debugPollTimer_.Tick([weak](auto&&, auto&&) { - if (const auto self = weak.get()) self->session_->pollDebugger(); + if (const auto self = weak.get(); self && + self->debugSnapshot_.state != lithe::windows::app::JavaDebugSessionState::Idle && + self->debugSnapshot_.state != lithe::windows::app::JavaDebugSessionState::Finished && + self->debugSnapshot_.state != lithe::windows::app::JavaDebugSessionState::Failed) { + self->session_->pollDebugger(); + } }); - debugPollTimer_.Start(); - workbenchSaveTimer_ = dispatcher_.CreateTimer(); workbenchSaveTimer_.Interval(std::chrono::milliseconds(350)); workbenchSaveTimer_.IsRepeating(false); workbenchSaveToken_ = workbenchSaveTimer_.Tick([weak](auto&&, auto&&) { - if (const auto self = weak.get()) self->saveWorkbenchState(); + if (const auto self = weak.get()) { + self->workbenchSaveTimer_.Stop(); + self->saveWorkbenchState(); + } }); markdownPreviewTimer_ = dispatcher_.CreateTimer(); @@ -3317,16 +3325,34 @@ void MainWindow::CheckForUpdatesClick(IInspectable const&, RoutedEventArgs const } void MainWindow::ProjectItemInvoked( - TreeView const&, TreeViewItemInvokedEventArgs const& event) { - const auto node = event.InvokedItem().try_as(); + TreeView const& tree, TreeViewItemInvokedEventArgs const& event) { + auto node = event.InvokedItem().try_as(); + if (!node) { + const auto container = tree.ContainerFromItem(event.InvokedItem()); + if (container) node = tree.NodeFromContainer(container); + } + if (!node) node = tree.SelectedNode(); if (!node) return; const auto found = treePaths_.find(objectKey(node)); if (found == treePaths_.end()) return; - if (directoryPaths_.contains(found->second)) { - node.IsExpanded(!node.IsExpanded()); - return; - } - openDocument(found->second); + if (directoryPaths_.contains(found->second)) return; + const auto path = found->second; + event.Handled(true); + const auto weak = get_weak(); + dispatcher_.TryEnqueue([weak, path] { + if (const auto self = weak.get()) self->openDocument(path); + }); +} + +void MainWindow::ProjectTreeExpanding( + TreeView const&, TreeViewExpandingEventArgs const& event) { + populateWorkspaceChildren(event.Node()); + scheduleWorkbenchStateSave(); +} + +void MainWindow::ProjectTreeCollapsed( + TreeView const&, TreeViewCollapsedEventArgs const&) { + scheduleWorkbenchStateSave(); } void MainWindow::ProjectTreeRightTapped( @@ -3514,8 +3540,13 @@ void MainWindow::RevealSelectedItemClick(IInspectable const&, RoutedEventArgs co void MainWindow::ChangeItemClick(IInspectable const&, ItemClickEventArgs const& event) { const auto path = itemTag(event.ClickedItem()); if (path.empty()) return; - openDocument(path); - session_->loadDiff({path}); + const auto weak = get_weak(); + dispatcher_.TryEnqueue([weak, path] { + if (const auto self = weak.get()) { + self->openDocument(path); + self->session_->loadDiff({path}); + } + }); } void MainWindow::SearchResultClick(IInspectable const&, ItemClickEventArgs const& event) { @@ -3550,7 +3581,13 @@ void MainWindow::GitHistoryItemClick(IInspectable const&, ItemClickEventArgs con void MainWindow::CommitFileClick(IInspectable const&, ItemClickEventArgs const& event) { const auto path = itemTag(event.ClickedItem()); if (!path.empty() && !selectedGitCommit_.empty()) { - session_->loadGitCommitDiff(selectedGitCommit_, path); + const auto weak = get_weak(); + const auto commit = selectedGitCommit_; + dispatcher_.TryEnqueue([weak, commit, path] { + if (const auto self = weak.get()) { + self->session_->loadGitCommitDiff(commit, path); + } + }); } } @@ -3700,7 +3737,12 @@ void MainWindow::RootKeyDown(IInspectable const&, Input::KeyRoutedEventArgs cons void MainWindow::EditorTextChanged(IInspectable const&, RoutedEventArgs const&) { if (editorUpdating_ || editorFormatting_ || activePath_.empty() || isExternalDocument(activePath_)) return; - openDocuments_[activePath_] = editorText(); + auto source = editorText(); + if (const auto current = openDocuments_.find(activePath_); + current != openDocuments_.end() && current->second == source) { + return; + } + openDocuments_[activePath_] = std::move(source); dirtyPaths_.insert(activePath_); session_->setDocumentText(openDocuments_[activePath_]); if (isJavaPath(activePath_)) { @@ -4486,7 +4528,10 @@ void MainWindow::restoreWorkbenchState() { std::unordered_set expanded(state.expandedPaths.begin(), state.expandedPaths.end()); std::function expand = [&](TreeViewNode const& node) { const auto found = treePaths_.find(objectKey(node)); - if (found != treePaths_.end() && expanded.contains(found->second)) node.IsExpanded(true); + if (found != treePaths_.end() && expanded.contains(found->second)) { + populateWorkspaceChildren(node); + node.IsExpanded(true); + } for (const auto& child : node.Children()) expand(child); }; for (const auto& root : ProjectTree().RootNodes()) expand(root); @@ -4718,7 +4763,8 @@ void MainWindow::applySyntaxHighlighting() { const auto selectionEnd = selection.EndPosition(); const auto offsets = utf16Offsets(source); const auto entire = EditorTextBox().Document().GetRange(0, offsets.back()); - entire.CharacterFormat().ForegroundColor(applicationColor(L"LitheCodeTextColor")); + const auto theme = EditorTextBox().ActualTheme(); + entire.CharacterFormat().ForegroundColor(applicationColor(L"LitheCodeTextColor", theme)); for (const auto& span : lithe::windows::algorithms::highlightSyntax(source)) { if (span.start >= offsets.size() || span.end >= offsets.size()) continue; const auto start = offsets[span.start]; @@ -4739,7 +4785,7 @@ void MainWindow::applySyntaxHighlighting() { case lithe::windows::algorithms::SyntaxHighlightKind::Comment: color = L"LitheCodeCommentColor"; break; } - range.CharacterFormat().ForegroundColor(applicationColor(color)); + range.CharacterFormat().ForegroundColor(applicationColor(color, theme)); } selection.SetRange(selectionStart, selectionEnd); editorFormatting_ = false; @@ -4873,11 +4919,31 @@ void MainWindow::renderWorkspace(lithe::windows::app::WorkspaceFeatureState stat return; } if (state.error || !state.snapshot) return; + std::unordered_set expandedPaths; + std::function collectExpanded = [&](TreeViewNode const& node) { + const auto found = treePaths_.find(objectKey(node)); + if (node.IsExpanded() && found != treePaths_.end()) expandedPaths.insert(found->second); + for (const auto& child : node.Children()) collectExpanded(child); + }; + for (const auto& root : ProjectTree().RootNodes()) collectExpanded(root); + showWorkbenchSurface(); treePaths_.clear(); directoryPaths_.clear(); + workspaceNodes_.clear(); ProjectTree().RootNodes().Clear(); - appendWorkspaceNode(nullptr, state.snapshot->root, true); + workspaceSnapshot_ = *state.snapshot; + indexWorkspaceNode(workspaceSnapshot_->root); + appendWorkspaceNode(nullptr, workspaceSnapshot_->root, true); + std::function restoreExpanded = [&](TreeViewNode const& node) { + const auto found = treePaths_.find(objectKey(node)); + if (found != treePaths_.end() && expandedPaths.contains(found->second)) { + populateWorkspaceChildren(node); + node.IsExpanded(true); + } + for (const auto& child : node.Children()) restoreExpanded(child); + }; + for (const auto& root : ProjectTree().RootNodes()) restoreExpanded(root); const auto root = state.root.value_or(std::filesystem::path{}); const auto rootName = root.empty() ? std::string{} : fileName(pathUtf8(root)); WorkspaceTitleText().Text(rootName.empty() ? L"" : text("- " + rootName)); @@ -4887,28 +4953,32 @@ void MainWindow::renderWorkspace(lithe::windows::app::WorkspaceFeatureState stat countLabel(state.snapshot->files.size(), "files", "文件", simplifiedChinese_)); } +void MainWindow::indexWorkspaceNode(const lithe::windows::WorkspaceNodeDto& item) { + workspaceNodes_[item.path] = &item; + for (const auto& child : item.children) indexWorkspaceNode(child); +} + +void MainWindow::populateWorkspaceChildren(TreeViewNode const& node) { + if (!node || node.Children().Size() != 0) return; + const auto path = treePaths_.find(objectKey(node)); + if (path == treePaths_.end()) return; + const auto item = workspaceNodes_.find(path->second); + if (item == workspaceNodes_.end()) return; + node.HasUnrealizedChildren(false); + for (const auto& child : item->second->children) appendWorkspaceNode(node, child); +} + void MainWindow::appendWorkspaceNode( TreeViewNode const& parent, const lithe::windows::WorkspaceNodeDto& item, bool isRoot) { TreeViewNode node; - StackPanel label; - label.Orientation(Orientation::Horizontal); - label.Spacing(7); - FontIcon icon; - icon.Glyph(item.isDirectory ? L"\xE8B7" : L"\xE8A5"); - icon.FontSize(12); - icon.Foreground(applicationBrush(L"LitheMutedTextBrush")); - label.Children().Append(icon); - TextBlock name; - name.Text(text(item.name.empty() ? item.path : item.name)); - name.TextTrimming(TextTrimming::CharacterEllipsis); - label.Children().Append(name); - node.Content(label); + node.Content(box_value(text(item.name.empty() ? item.path : item.name))); node.IsExpanded(isRoot); + node.HasUnrealizedChildren(item.isDirectory && !item.children.empty()); treePaths_[objectKey(node)] = item.path; if (item.isDirectory) directoryPaths_.insert(item.path); if (parent) parent.Children().Append(node); else ProjectTree().RootNodes().Append(node); - for (const auto& child : item.children) appendWorkspaceNode(node, child); + if (isRoot) populateWorkspaceChildren(node); } void MainWindow::renderDocument(lithe::windows::app::DocumentFeatureState state) { @@ -5348,6 +5418,12 @@ void MainWindow::appendDebugVariable( void MainWindow::renderJavaDebug(lithe::windows::app::JavaDebugSnapshot snapshot) { debugSnapshot_ = snapshot; + const bool debugActive = + snapshot.state != lithe::windows::app::JavaDebugSessionState::Idle && + snapshot.state != lithe::windows::app::JavaDebugSessionState::Finished && + snapshot.state != lithe::windows::app::JavaDebugSessionState::Failed; + if (debugActive) debugPollTimer_.Start(); + else debugPollTimer_.Stop(); debugVariableIDs_.clear(); DebugVariablesList().Items().Clear(); DebugThreadsList().Items().Clear(); diff --git a/windows/winui/MainWindow.xaml.h b/windows/winui/MainWindow.xaml.h index f937868fe..521a91ce7 100644 --- a/windows/winui/MainWindow.xaml.h +++ b/windows/winui/MainWindow.xaml.h @@ -144,6 +144,12 @@ struct MainWindow : MainWindowT { void ProjectItemInvoked( Microsoft::UI::Xaml::Controls::TreeView const&, Microsoft::UI::Xaml::Controls::TreeViewItemInvokedEventArgs const&); + void ProjectTreeExpanding( + Microsoft::UI::Xaml::Controls::TreeView const&, + Microsoft::UI::Xaml::Controls::TreeViewExpandingEventArgs const&); + void ProjectTreeCollapsed( + Microsoft::UI::Xaml::Controls::TreeView const&, + Microsoft::UI::Xaml::Controls::TreeViewCollapsedEventArgs const&); void ProjectTreeRightTapped( IInspectable const&, Microsoft::UI::Xaml::Input::RightTappedRoutedEventArgs const&); winrt::fire_and_forget NewFileClick( @@ -322,6 +328,9 @@ struct MainWindow : MainWindowT { Microsoft::UI::Xaml::Controls::TreeViewNode const& parent, const lithe::windows::WorkspaceNodeDto& node, bool isRoot = false); + void indexWorkspaceNode(const lithe::windows::WorkspaceNodeDto& node); + void populateWorkspaceChildren( + Microsoft::UI::Xaml::Controls::TreeViewNode const& node); void renderDocument(lithe::windows::app::DocumentFeatureState state); void renderSearch(lithe::windows::app::SearchFeatureState state); void renderSearchEverywhere(lithe::windows::app::SearchEverywhereFeatureState state); @@ -361,6 +370,8 @@ struct MainWindow : MainWindowT { HWND hwnd_{nullptr}; Microsoft::UI::Dispatching::DispatcherQueue dispatcher_{nullptr}; std::unique_ptr session_; + std::optional workspaceSnapshot_; + std::unordered_map workspaceNodes_; std::unordered_map treePaths_; std::unordered_set directoryPaths_; std::unordered_map navigationTargets_; diff --git a/windows/winui/workbench_session.cpp b/windows/winui/workbench_session.cpp index 42a16366c..338ed252a 100644 --- a/windows/winui/workbench_session.cpp +++ b/windows/winui/workbench_session.cpp @@ -306,7 +306,15 @@ void WorkbenchSession::openWorkspace(std::filesystem::path root) { watchedRoot, [this, watchedRoot](const std::vector& changes) { if (pathUtf8(workspaceRoot_) != watchedRoot) return; - auto visibleChanges = writeLifecycle_->observeChanges(changes); + std::vector workspaceChanges; + workspaceChanges.reserve(changes.size()); + std::copy_if(changes.begin(), changes.end(), + std::back_inserter(workspaceChanges), [](const auto& change) { + const auto normalized = normalizedSlashes(change.path); + return normalized != ".git" && !normalized.starts_with(".git/"); + }); + if (workspaceChanges.empty()) return; + auto visibleChanges = writeLifecycle_->observeChanges(std::move(workspaceChanges)); if (!visibleChanges) return; if (callbacks_.filesChanged) callbacks_.filesChanged(std::move(*visibleChanges)); refreshAfterWrite();