Compare commits

..

22 commits

Author SHA1 Message Date
834554167e chore: fige Cargo.lock (epoch-1.10.0, [patch] locaux)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 07:02:21 +02:00
79f6777d08 merge: epoch-1.10.0 d'upstream
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 07:00:05 +02:00
Michael Murphy
5ddf511599
i18n: translation update from Hosted Weblate (#631)
Translations update from [Hosted Weblate](https://hosted.weblate.org)
for [Pop OS/COSMIC
Edit](https://hosted.weblate.org/projects/pop-os/cosmic-edit/).



Current translation status:

![Weblate translation
status](https://hosted.weblate.org/widget/pop-os/cosmic-edit/matrix-auto.svg)
2026-10-06 21:55:46 +02:00
Michael Aaron Murphy
35b6a05bc1 Epoch 1.10.0 version update
[x] Generated by cosmic-epoch scripts/version-update.sh
2026-10-06 21:55:24 +02:00
Hojjat
1398716508 chore: clean up formatting and lint 2026-09-30 13:41:31 -06:00
Hojjat
d7b82fad91 chore: set up github actions for fmt, test, and clippy 2026-09-30 13:41:31 -06:00
Kartik
690065d9a8
Add license url & update developers name to System76 (#635)
- [X] I have disclosed use of any AI generated code in my commit
messages.
- If you are using an LLM, and do not fully understand the changes it is
making to the code base, do not create a PR.
- In our experience, AI generated code often results in overly complex
code that lacks enough context for a proper fix or feature inclusion.
This results in considerably longer code reviews. Due to this, AI
authored or partially authored PRs may be closed without comment.
- [X] I understand these changes in full and will be able to respond to
review comments.
- [X] My change is accurately described in the commit message.
- [X] My contribution is tested and working as described.
- [X] I have read the [Developer Certificate of
Origin](https://developercertificate.org/) and certify my contribution
under its conditions.
2026-09-30 13:29:08 -06:00
Jeremy Soller
7baa24b649 COSMIC policy update
Generated by cosmic-epoch scripts/policy-update.sh. Uses `policy_nobuild` branch to avoid package builds as it contains no code changes.
2026-09-30 09:55:31 -06:00
936e57b63e chore: fige Cargo.lock (epoch-1.9.0, [patch] locaux)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-24 21:03:54 +02:00
c45b23aa85 merge: epoch-1.9.0 d'upstream
# Conflicts:
#	Cargo.lock
2026-09-24 20:50:47 +02:00
Hojjat
bfc98bf219 chore: update libcosmic to fix esc, and bindings in context menu 2026-09-23 11:30:54 -06:00
Hosted Weblate
4b38bdf398
i18n: translation updates from weblate
Co-authored-by: therealmate <hellogaming91@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-edit/hu/
Translation: Pop OS/COSMIC Edit
2026-09-19 10:51:35 +02:00
Michael Aaron Murphy
14aba66228 chore: update debian/rules 2026-09-16 16:28:59 -04:00
Michael Aaron Murphy
430120f6ff chore: update cosmic-files and libcosmic 2026-09-16 16:28:59 -04:00
Hojjat
9307793661 feat: use libcosmic's context_menu widget 2026-09-16 16:28:59 -04:00
Hojjat
505ecd18fe chore: adopt typed surface actions from libcosmic 2026-09-16 16:28:59 -04:00
Michael Aaron Murphy
4b4871ecf4 Epoch 1.9.0 version update
Generated by cosmic-epoch scripts/version-update.sh
2026-09-16 16:28:59 -04:00
Michael Murphy
1054b6a7fd
i18n: translation update from Hosted Weblate (#616)
Translations update from [Hosted Weblate](https://hosted.weblate.org)
for [Pop OS/COSMIC
Edit](https://hosted.weblate.org/projects/pop-os/cosmic-edit/).



Current translation status:

![Weblate translation
status](https://hosted.weblate.org/widget/pop-os/cosmic-edit/matrix-auto.svg)
2026-09-15 21:56:46 +02:00
Hosted Weblate
139cfaaf5c
i18n: translation updates from weblate
Co-authored-by: Amadɣas <massiin@proton.me>
Co-authored-by: Blauen <butterflee555@gmail.com>
Co-authored-by: CYAXXX <85353920+CYAXXX@users.noreply.github.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Kurt Harren Catindig <kurtharrencatindig03@gmail.com>
Co-authored-by: Quentin PAGÈS <quentinantonin@free.fr>
Co-authored-by: Satakun Utama <galaxytomostore@gmail.com>
Co-authored-by: VandaL <vandalhj@gmail.com>
Co-authored-by: therealmate <hellogaming91@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-edit/hu/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-edit/kab/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-edit/kmr/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-edit/oc/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-edit/pl/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-edit/th/
Translation: Pop OS/COSMIC Edit
2026-09-13 01:54:35 +02:00
85da7f90f2 chore: fige Cargo.lock 2026-09-10 18:12:37 +02:00
70c1765b1f Merge tag 'epoch-1.8.0' into local/libcosmic-path-patch
Epoch 1.8.0

# Conflicts:
#	Cargo.lock
2026-09-10 17:57:28 +02:00
Jeremy Soller
26f6ffd3e6 Epoch 1.8.0 version update
Generated by cosmic-epoch scripts/version-update.sh
2026-09-03 13:18:38 -06:00
25 changed files with 823 additions and 745 deletions

View file

@ -1,8 +1,10 @@
- [ ] I have disclosed use of any AI generated code in my commit messages.
- If you are using an LLM, and do not fully understand the changes it is making to the code base, do not create a PR.
- In our experience, AI generated code often results in overly complex code that lacks enough context for a proper fix or feature inclusion. This results in considerably longer code reviews. Due to this, AI authored or partially authored PRs may be closed without comment.
- [ ] I understand these changes in full and will be able to respond to review comments.
- [ ] My change is accurately described in the commit message.
- [ ] My contribution is tested and working as described.
- [ ] I have read the [Developer Certificate of Origin](https://developercertificate.org/) and certify my contribution under its conditions.
Replace this text with a description of your changes, and why these changes are important. Read and acknowledge the list below. PRs without a completed checkbox will be closed.
---
- I have not included any LLM (also known as AI) generated content in this PR, including code, comments, and descriptions.
- I understand these changes in full and will be able to respond to review comments.
- My change is accurately described in the commit message.
- My contribution is tested and working as described.
- I have read the [Developer Certificate of Origin](https://developercertificate.org/) and certify my contribution under its conditions.
- [ ] I have read and followed all of the above requirements. For more information, see our [contributor guide](https://github.com/pop-os/pop/blob/master/CONTRIBUTING.md).

47
.github/workflows/ci.yml vendored Normal file
View file

@ -0,0 +1,47 @@
name: Continuous integration
on:
push:
branches: [master]
pull_request:
env:
CARGO_TERM_COLOR: always
DEBIAN_FRONTEND: noninteractive
jobs:
fmt:
name: Rustfmt
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v5
- name: install toolchain
run: rustup toolchain install nightly --component rustfmt
- name: fmt
run: cargo +nightly fmt --all --check
clippy:
name: Clippy
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v5
- name: install system dependencies
run: sudo apt-get update && sudo apt-get install -y pkg-config libglib2.0-dev libxkbcommon-dev libwayland-dev
- name: install toolchain
run: rustup show
- uses: Swatinem/rust-cache@v2
- name: clippy
run: cargo clippy --all-features --all-targets -- -D warnings
test:
name: Test
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v5
- name: install system dependencies
run: sudo apt-get update && sudo apt-get install -y pkg-config libglib2.0-dev libxkbcommon-dev libwayland-dev
- name: install toolchain
run: rustup show
- uses: Swatinem/rust-cache@v2
- name: test
run: cargo test --all-features

14
.github/workflows/require-checklist.yml vendored Normal file
View file

@ -0,0 +1,14 @@
# From https://github.com/mheap/require-checklist-action, license MIT
name: Require Checklist
on:
pull_request:
types: [opened, edited, synchronize]
jobs:
job1:
runs-on: ubuntu-latest
steps:
- uses: mheap/require-checklist-action@v2
with:
requireChecklist: true

817
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
[package]
name = "cosmic-edit"
version = "1.7.0"
version = "1.10.0"
authors = ["Jeremy Soller <jeremy@system76.com>"]
edition = "2024"
license = "GPL-3.0-only"

70
cargo.just Normal file
View file

@ -0,0 +1,70 @@
cargo-target-dir := env('CARGO_TARGET_DIR', 'target')
# Compile with debug profile
[no-cd]
build-debug *args:
cargo build {{args}}
# Compile with release profile
[no-cd]
build-release *args: (build-debug '--release' args)
# Compile with a vendored tarball
[no-cd]
build-vendored *args: vendor-extract (build-release '--frozen --offline' args)
# Check for errors and linter warnings
[no-cd]
check *args:
cargo clippy --all-features {{args}} -- -W clippy::pedantic
# Runs a check with JSON message format for IDE integration
[no-cd]
check-json: (check '--message-format=json')
# Remove Cargo build artifacts
[no-cd]
clean:
cargo clean
# Also remove .cargo and vendored dependencies
[no-cd]
clean-dist: clean
rm -rf .cargo vendor vendor.tar target
# Run the application for testing purposes
[no-cd]
run *args:
cargo run {{args}}
# Run `cargo test`
[no-cd]
test *args:
cargo test {{args}}
# Vendor Cargo dependencies locally
[no-cd]
vendor:
#!/usr/bin/env bash
mkdir -p .cargo
cargo vendor --locked | head -n -1 > .cargo/config.toml
echo 'directory = "vendor"' >> .cargo/config.toml
echo >> .cargo/config.toml
echo '[env]' >> .cargo/config.toml
if [ -n "${SOURCE_DATE_EPOCH}" ]
then
source_date="$(date -d "@${SOURCE_DATE_EPOCH}" "+%Y-%m-%d")"
echo "VERGEN_GIT_COMMIT_DATE = \"${source_date}\"" >> .cargo/config.toml
fi
if [ -n "${SOURCE_GIT_HASH}" ]
then
echo "VERGEN_GIT_SHA = \"${SOURCE_GIT_HASH}\"" >> .cargo/config.toml
fi
tar pcf vendor.tar .cargo vendor
rm -rf .cargo vendor
# Extracts vendored dependencies
[no-cd]
vendor-extract:
rm -rf vendor
tar pxf vendor.tar

18
debian/changelog vendored
View file

@ -1,3 +1,21 @@
cosmic-edit (1.10.0) noble; urgency=medium
* Epoch 1.10.0 version update
-- Michael Murphy <michael@mmurphy.dev> Mon, 05 Oct 2026 21:19:33 +0200
cosmic-edit (1.9.0) noble; urgency=medium
* Epoch 1.9.0 version update
-- Michael Murphy <michael@mmurphy.dev> Mon, 14 Sep 2026 21:54:06 +0200
cosmic-edit (1.8.0) noble; urgency=medium
* Epoch 1.8.0 version update
-- Jeremy Soller <jeremy@system76.com> Wed, 02 Sep 2026 08:14:30 -0600
cosmic-edit (1.7.0) noble; urgency=medium
* Epoch 1.7.0 version update

5
debian/rules vendored
View file

@ -1,15 +1,12 @@
#!/usr/bin/make -f
export DESTDIR = debian/cosmic-edit
export VENDOR ?= 1
%:
dh $@
override_dh_auto_clean:
if ! ischroot && test "${VENDOR}" = "1"; then \
just vendor; \
fi
ischroot || just vendor
override_dh_auto_build:
just build-vendored

View file

@ -1,4 +1,6 @@
tar-ignore=.github
tar-ignore=.vscode
tar-ignore=target/.rustc_info.json
tar-ignore=target/CACHEDIR.TAG
tar-ignore=target/debug
tar-ignore=target/release
tar-ignore=vendor
tar-ignore=target

0
i18n/fil/cosmic_edit.ftl Normal file
View file

View file

@ -53,14 +53,14 @@ dark = Sötét
light = Világos
syntax-dark = Sötét szintaxis
syntax-light = Világos szintaxis
default-font = Alapértelmezett betűtípus
default-font = Alapértelmezett betűkészlet
default-font-size = Alapértelmezett betűméret
default-zoom-step = Nagyítási mérték
### Keyboard shortcuts
keyboard-shortcuts = Gyorsbillentyűk
enable-vim-bindings = Vim billentyűkombinációk engedélyezése
enable-vim-bindings = Vim-billentyűkombinációk engedélyezése
# Find
find-placeholder = Keresés…
find-previous = Előző találat

View file

@ -1,9 +1,9 @@
cancel = Sefsex
cancel = Semmet
undo = Ssemmet
save = Sekles
quit = Tuffɣa
quit = Ffeɣ
replace = Semselsi
support = Tallalt
support = Tallelt
settings = Iɣewwaṛen
appearance = Timeẓri
theme = Asentel
@ -16,7 +16,7 @@ default-size = Tiddi tamezwert
zoom-out = Asemẓi
view = Wali
menu-settings = Iɣewwaṛen…
match-desktop = Amṣada d tnarit
match-desktop = Semṣada akked tnarit
file = Afaylu
new-window = Asfaylu amaynut
edit = Ẓreg

View file

@ -12,7 +12,7 @@ support = Piştgirî
quit = Biqedîne
find-placeholder = Bibîne...
copy = Jê bigire
paste = Pêve bike
paste = Pê ve bike
match-desktop = Bi sermaseyê re li hev bîne
select-all = Hemûyan hilbijêre
zoom-in = Nêzîk bike

View file

@ -1,8 +1,13 @@
settings = Paramètres
settings = Configuracion
appearance = Aparéncia
theme = Tèma
match-desktop = Assortir al burèu
dark = Fosc
dark = Escur
light = Clar
file = Fichièr
close-file = Tampar lo fichièr
cancel = Anullar
repository = Depaus
menu-settings = Configuracion...
quit = Quitar
replace = Remplaçar

View file

@ -1,5 +1,5 @@
cosmic-text-editor = Edytor Tekstu COSMIC
comment = Edytor tekstu pulpitu COSMIC
comment = Edytor tekstu dla środowiska COSMIC
new-document = Nowy dokument
open-project = Otwórz projekt
todo = TODO
@ -64,7 +64,7 @@ default-zoom-step = Stopniowanie powiększania
keyboard-shortcuts = Skróty klawiszowe
enable-vim-bindings = Włącz tryb Vim
# Find
find-placeholder = Szukaj…
find-placeholder = Wyszukaj…
find-previous = Znajdź poprzedni
find-next = Znajdź następny
replace-placeholder = Zastąp…
@ -106,8 +106,8 @@ cut = Wytnij
copy = Kopiuj
paste = Wklej
select-all = Zaznacz wszystko
find = Szukaj
find-in-project = Szukaj w projekcie…
find = Wyszukaj
find-in-project = Wyszukaj w projekcie…
spell-check = Sprawdź ortografię…
## View

View file

@ -0,0 +1,6 @@
cancel = ยกเลิก
settings = การตั้งค่า
dark = มืด
light = สว่าง
replace = แทนที่
appearance = ลักษณะ

3
rust-toolchain.toml Normal file
View file

@ -0,0 +1,3 @@
[toolchain]
channel = "1.93"
components = ["clippy", "rustfmt"]

View file

@ -112,17 +112,8 @@ impl Config {
}
}
#[derive(Clone, CosmicConfigEntry, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[derive(Clone, CosmicConfigEntry, Debug, Deserialize, Eq, PartialEq, Serialize, Default)]
pub struct ConfigState {
pub recent_files: VecDeque<PathBuf>,
pub recent_projects: VecDeque<PathBuf>,
}
impl Default for ConfigState {
fn default() -> Self {
Self {
recent_files: VecDeque::new(),
recent_projects: VecDeque::new(),
}
}
}

View file

@ -124,7 +124,7 @@ impl GitRepository {
msg.push_str("\nstderr> ");
msg.push_str(line);
}
Err(io::Error::new(io::ErrorKind::Other, msg))
Err(io::Error::other(msg))
}
}

View file

@ -11,7 +11,7 @@ use cosmic::{
cosmic_theme, executor,
font::Font,
iced::{
self, Alignment, Background, Color, Length, Limits, Point, Subscription,
self, Alignment, Background, Color, Length, Limits, Subscription,
advanced::graphics::text::font_system,
clipboard, event,
futures::{self, SinkExt},
@ -328,6 +328,7 @@ impl PartialEq for WatcherWrapper {
}
}
#[allow(clippy::large_enum_variant)]
enum NewTab {
Tab(EditorTab),
Exists(Entity),
@ -403,7 +404,7 @@ pub enum Message {
SaveAsResult(segmented_button::Entity, DialogResult),
Scroll(f32),
SelectAll,
Surface(surface::Action),
Surface(surface::Action<Message>),
SystemThemeModeChange(cosmic_theme::ThemeMode),
SyntaxTheme(usize, bool),
TabActivate(segmented_button::Entity),
@ -412,7 +413,7 @@ pub enum Message {
TabClose(segmented_button::Entity),
TabCloseForce(segmented_button::Entity),
TabContextAction(segmented_button::Entity, Action),
TabContextMenu(segmented_button::Entity, Option<Point>),
TabContextMenu(segmented_button::Entity, bool),
TabNext,
TabPrev,
TabSetCursor(segmented_button::Entity, Cursor),
@ -529,7 +530,7 @@ impl App {
continue;
}
let entry_path = entry.path();
let node = match ProjectNode::new(&entry_path) {
let node = match ProjectNode::new(entry_path) {
Ok(ok) => ok,
Err(err) => {
log::error!(
@ -682,11 +683,11 @@ impl App {
//TODO: allow files to be open multiple times
let mut activate_opt = None;
for entity in self.tab_model.iter() {
if let Some(Tab::Editor(tab)) = self.tab_model.data::<Tab>(entity) {
if tab.path_opt.as_ref() == Some(&canonical) {
activate_opt = Some(entity);
break;
}
if let Some(Tab::Editor(tab)) = self.tab_model.data::<Tab>(entity)
&& tab.path_opt.as_ref() == Some(&canonical)
{
activate_opt = Some(entity);
break;
}
}
if let Some(entity) = activate_opt {
@ -734,10 +735,10 @@ impl App {
}
let entities: Vec<_> = self.tab_model.iter().collect();
for entity in entities {
if self.tab_model.is_active(entity) {
if let Some(Tab::Editor(tab)) = self.tab_model.data_mut::<Tab>(entity) {
tab.set_config(&self.config);
}
if self.tab_model.is_active(entity)
&& let Some(Tab::Editor(tab)) = self.tab_model.data_mut::<Tab>(entity)
{
tab.set_config(&self.config);
}
}
}
@ -754,10 +755,10 @@ impl App {
}
fn save_config_state(&mut self) {
if let Some(ref config_state_handler) = self.config_state_handler {
if let Err(err) = self.config_state.write_entry(config_state_handler) {
log::error!("failed to save config_state: {}", err);
}
if let Some(ref config_state_handler) = self.config_state_handler
&& let Err(err) = self.config_state.write_entry(config_state_handler)
{
log::error!("failed to save config_state: {}", err);
}
}
@ -777,10 +778,10 @@ impl App {
Some(DialogPage::PromptSaveQuit(ref _entities)) => {
let mut unsaved = Vec::new();
for entity in self.tab_model.iter() {
if let Some(Tab::Editor(tab)) = self.tab_model.data::<Tab>(entity) {
if tab.changed() {
unsaved.push(entity);
}
if let Some(Tab::Editor(tab)) = self.tab_model.data::<Tab>(entity)
&& tab.changed()
{
unsaved.push(entity);
}
}
if unsaved.is_empty() {
@ -919,16 +920,16 @@ impl App {
}
'tabs: for entity in self.tab_model.iter() {
if let Some(Tab::Editor(tab)) = self.tab_model.data::<Tab>(entity) {
if let Some(path) = &tab.path_opt {
for (_, project_path) in self.projects.iter() {
if path.starts_with(&project_path) {
// Do not watch tabs inside of already watched projects
continue 'tabs;
}
if let Some(Tab::Editor(tab)) = self.tab_model.data::<Tab>(entity)
&& let Some(path) = &tab.path_opt
{
for (_, project_path) in self.projects.iter() {
if path.starts_with(project_path) {
// Do not watch tabs inside of already watched projects
continue 'tabs;
}
new_paths.insert((path.to_path_buf(), RecursiveMode::NonRecursive));
}
new_paths.insert((path.to_path_buf(), RecursiveMode::NonRecursive));
}
}
@ -1443,7 +1444,7 @@ impl Application for App {
let mut theme_names =
Vec::with_capacity(SYNTAX_SYSTEM.get().unwrap().theme_set.themes.len());
for (theme_name, _theme) in SYNTAX_SYSTEM.get().unwrap().theme_set.themes.iter() {
for theme_name in SYNTAX_SYSTEM.get().unwrap().theme_set.themes.keys() {
theme_names.push(theme_name.to_string());
}
@ -1462,7 +1463,7 @@ impl Application for App {
.comments(fl!("comment"))
.license("GPL-3.0-only")
.license_url("https://spdx.org/licenses/GPL-3.0-only")
.developers([("Jeremy Soller", "jeremy@system76.com")])
.developers([("System76", "info@system76.com")])
.links([
(fl!("repository"), "https://github.com/pop-os/cosmic-edit"),
(
@ -1652,9 +1653,7 @@ impl Application for App {
}
fn dialog(&self) -> Option<Element<'_, Self::Message>> {
let Some(ref dialog) = self.dialog_page_opt else {
return None;
};
let dialog = self.dialog_page_opt.as_ref()?;
let cosmic_theme::Spacing { space_xxs, .. } = self.core().system_theme().cosmic().spacing;
@ -1937,21 +1936,21 @@ impl Application for App {
return self.update_config();
}
Message::FindNext => {
if !self.find_search_value.is_empty() {
if let Some(Tab::Editor(tab)) = self.active_tab() {
//TODO: do not compile find regex on every search?
match self.config.find_regex(&self.find_search_value) {
Ok(regex) => {
tab.search(&regex, true, self.config.find_wrap_around);
}
Err(err) => {
//TODO: put regex error in find box
log::warn!(
"failed to compile regex {:?}: {}",
self.find_search_value,
err
);
}
if !self.find_search_value.is_empty()
&& let Some(Tab::Editor(tab)) = self.active_tab()
{
//TODO: do not compile find regex on every search?
match self.config.find_regex(&self.find_search_value) {
Ok(regex) => {
tab.search(&regex, true, self.config.find_wrap_around);
}
Err(err) => {
//TODO: put regex error in find box
log::warn!(
"failed to compile regex {:?}: {}",
self.find_search_value,
err
);
}
}
}
@ -1960,21 +1959,21 @@ impl Application for App {
return self.update_focus();
}
Message::FindPrevious => {
if !self.find_search_value.is_empty() {
if let Some(Tab::Editor(tab)) = self.active_tab() {
//TODO: do not compile find regex on every search?
match self.config.find_regex(&self.find_search_value) {
Ok(regex) => {
tab.search(&regex, false, self.config.find_wrap_around);
}
Err(err) => {
//TODO: put regex error in find box
log::warn!(
"failed to compile regex {:?}: {}",
self.find_search_value,
err
);
}
if !self.find_search_value.is_empty()
&& let Some(Tab::Editor(tab)) = self.active_tab()
{
//TODO: do not compile find regex on every search?
match self.config.find_regex(&self.find_search_value) {
Ok(regex) => {
tab.search(&regex, false, self.config.find_wrap_around);
}
Err(err) => {
//TODO: put regex error in find box
log::warn!(
"failed to compile regex {:?}: {}",
self.find_search_value,
err
);
}
}
}
@ -1983,27 +1982,27 @@ impl Application for App {
return self.update_focus();
}
Message::FindReplace => {
if !self.find_search_value.is_empty() {
if let Some(Tab::Editor(tab)) = self.active_tab() {
//TODO: do not compile find regex on every search?
match self.config.find_regex(&self.find_search_value) {
Ok(regex) => {
//TODO: support captures
tab.replace(
&regex,
&self.find_replace_value,
self.config.find_wrap_around,
);
return self.update(Message::TabChanged(self.tab_model.active()));
}
Err(err) => {
//TODO: put regex error in find box
log::warn!(
"failed to compile regex {:?}: {}",
self.find_search_value,
err
);
}
if !self.find_search_value.is_empty()
&& let Some(Tab::Editor(tab)) = self.active_tab()
{
//TODO: do not compile find regex on every search?
match self.config.find_regex(&self.find_search_value) {
Ok(regex) => {
//TODO: support captures
tab.replace(
&regex,
&self.find_replace_value,
self.config.find_wrap_around,
);
return self.update(Message::TabChanged(self.tab_model.active()));
}
Err(err) => {
//TODO: put regex error in find box
log::warn!(
"failed to compile regex {:?}: {}",
self.find_search_value,
err
);
}
}
}
@ -2012,27 +2011,27 @@ impl Application for App {
return self.update_focus();
}
Message::FindReplaceAll => {
if !self.find_search_value.is_empty() {
if let Some(Tab::Editor(tab)) = self.active_tab() {
//TODO: do not compile find regex on every search?
match self.config.find_regex(&self.find_search_value) {
Ok(regex) => {
//TODO: support captures
{
let mut editor = tab.editor.lock().unwrap();
editor.set_cursor(cosmic_text::Cursor::new(0, 0));
}
while tab.replace(&regex, &self.find_replace_value, false) {}
return self.update(Message::TabChanged(self.tab_model.active()));
}
Err(err) => {
//TODO: put regex error in find box
log::warn!(
"failed to compile regex {:?}: {}",
self.find_search_value,
err
);
if !self.find_search_value.is_empty()
&& let Some(Tab::Editor(tab)) = self.active_tab()
{
//TODO: do not compile find regex on every search?
match self.config.find_regex(&self.find_search_value) {
Ok(regex) => {
//TODO: support captures
{
let mut editor = tab.editor.lock().unwrap();
editor.set_cursor(cosmic_text::Cursor::new(0, 0));
}
while tab.replace(&regex, &self.find_replace_value, false) {}
return self.update(Message::TabChanged(self.tab_model.active()));
}
Err(err) => {
//TODO: put regex error in find box
log::warn!(
"failed to compile regex {:?}: {}",
self.find_search_value,
err
);
}
}
}
@ -2172,18 +2171,14 @@ impl Application for App {
// Reload tabs that changed
let mut tab_reload = Vec::new();
for entity in self.tab_model.iter() {
if let Some(Tab::Editor(tab)) = self.tab_model.data::<Tab>(entity) {
if let Some(path) = &tab.path_opt {
if event.paths.contains(path) {
if tab.changed() {
log::warn!(
"file changed externally before being saved: {:?}",
path
);
} else {
tab_reload.push(entity);
}
}
if let Some(Tab::Editor(tab)) = self.tab_model.data::<Tab>(entity)
&& let Some(path) = &tab.path_opt
&& event.paths.contains(path)
{
if tab.changed() {
log::warn!("file changed externally before being saved: {:?}", path);
} else {
tab_reload.push(entity);
}
}
}
@ -2281,7 +2276,7 @@ impl Application for App {
{
for (_, project_path) in self.projects.iter() {
for path in event.paths.iter() {
if let Ok(prefix) = path.strip_prefix(&project_path) {
if let Ok(prefix) = path.strip_prefix(project_path) {
// Manually ignore project .git folders
//TODO: use logic from ignore crate somehow?
if prefix.starts_with(".git") {
@ -2351,10 +2346,10 @@ impl Application for App {
{
let mut close = Vec::new();
for entity in self.tab_model.iter() {
if let Some(Tab::GitDiff(other_tab)) = self.tab_model.data::<Tab>(entity) {
if other_tab.diff.path == diff.path {
close.push(entity);
}
if let Some(Tab::GitDiff(other_tab)) = self.tab_model.data::<Tab>(entity)
&& other_tab.diff.path == diff.path
{
close.push(entity);
}
}
for entity in close {
@ -2672,10 +2667,10 @@ impl Application for App {
tab.path_opt = Some(paths.remove(0));
title_opt = Some(tab.title());
tab.save();
if let Some(path) = tab.path_opt.clone() {
if let Ok(canonical) = fs::canonicalize(&path) {
self.add_to_recents(&canonical);
}
if let Some(path) = tab.path_opt.clone()
&& let Ok(canonical) = fs::canonicalize(&path)
{
self.add_to_recents(&canonical);
}
}
if let Some(title) = title_opt {
@ -2713,9 +2708,7 @@ impl Application for App {
}
}
Message::Surface(a) => {
return cosmic::task::message(cosmic::Action::Cosmic(
cosmic::app::Action::Surface(a),
));
return cosmic::task::message(cosmic::Action::Surface(a));
}
Message::SystemThemeModeChange(_theme_mode) => {
return self.update_config();
@ -2823,15 +2816,15 @@ impl Application for App {
Message::TabContextAction(entity, action) => {
if let Some(Tab::Editor(tab)) = self.tab_model.data_mut::<Tab>(entity) {
// Close context menu
tab.context_menu = None;
tab.context_menu_open = false;
// Run action's message
return self.update(action.message(None));
}
}
Message::TabContextMenu(entity, position_opt) => {
Message::TabContextMenu(entity, open) => {
if let Some(Tab::Editor(tab)) = self.tab_model.data_mut::<Tab>(entity) {
// Update context menu
tab.context_menu = position_opt;
tab.context_menu_open = open;
}
}
Message::TabNext => {
@ -2853,13 +2846,7 @@ impl Application for App {
.tab_model
.position(self.tab_model.active())
.and_then(|i| (i as usize).checked_sub(1))
.unwrap_or_else(|| {
self.tab_model
.iter()
.count()
.checked_sub(1)
.unwrap_or_default()
});
.unwrap_or_else(|| self.tab_model.iter().count().saturating_sub(1));
let entity = self.tab_model.iter().nth(pos);
if let Some(entity) = entity {
@ -3093,24 +3080,25 @@ impl Application for App {
.on_focus(Message::FindFocused(false))
.on_auto_scroll(Message::AutoScroll)
.on_changed(Message::TabChanged(tab_id))
.has_context_menu(tab.context_menu.is_some())
.on_context_menu(move |position_opt| {
Message::TabContextMenu(tab_id, position_opt)
});
.has_context_menu(tab.context_menu_open)
.on_context_menu(move |open| Message::TabContextMenu(tab_id, open));
if self.config.highlight_current_line {
text_box = text_box.highlight_current_line();
}
if self.config.line_numbers {
text_box = text_box.line_numbers();
}
let mut popover = widget::popover(text_box);
let has_selection = tab.editor.lock().unwrap().selection() != Selection::None;
if let Some(point) = tab.context_menu {
popover = popover
.popup(menu::context_menu(&self.key_binds, tab_id, has_selection))
.position(widget::popover::Position::Point(point));
let mut context_menu = widget::context_menu(
text_box,
Some(menu::context_menu(&self.key_binds, tab_id, has_selection)),
)
.on_close(Message::TabContextMenu(tab_id, false))
.on_surface_action(Message::Surface);
if let Some(window_id) = self.core().main_window_id() {
context_menu = context_menu.window_id(window_id);
}
tab_column = tab_column.push(popover);
tab_column = tab_column.push(context_menu);
if self.config.vim_bindings {
let status = {
let editor = tab.editor.lock().unwrap();

View file

@ -1,21 +1,22 @@
// SPDX-License-Identifier: GPL-3.0-only
use cosmic::widget::menu::Item as MenuItem;
use cosmic::widget::menu::action::MenuAction;
use cosmic::widget::menu::key_bind::KeyBind;
use cosmic::{
Element,
app::Core,
iced::{
Background, Border, Length, advanced::widget::text::Style as TextStyle, widget::column,
},
theme,
widget::{
self, divider,
menu::{ItemHeight, ItemWidth, menu_button},
responsive_menu_bar, segmented_button, space,
self,
menu::{ItemHeight, ItemWidth},
responsive_menu_bar, segmented_button,
},
};
use std::{collections::HashMap, path::PathBuf, sync::LazyLock};
use std::{
collections::HashMap,
path::{Path, PathBuf},
sync::LazyLock,
};
use crate::{Action, Config, ConfigState, Message, fl};
@ -63,7 +64,7 @@ fn truncate_middle(value: &str, max_chars: usize) -> String {
)
}
fn format_recent_menu_path(path: &PathBuf, home_dir_opt: Option<&PathBuf>) -> String {
fn format_recent_menu_path(path: &Path, home_dir_opt: Option<&PathBuf>) -> String {
const ELLIPSIS: &str = "...";
let display = if let Some(home_dir) = home_dir_opt {
@ -116,81 +117,49 @@ fn format_recent_menu_path(path: &PathBuf, home_dir_opt: Option<&PathBuf>) -> St
truncate_middle(&display, RECENT_MENU_LABEL_MAX_CHARS)
}
pub fn context_menu<'a>(
/// A context menu action dispatched to the tab the menu was opened on.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct TabAction(pub segmented_button::Entity, pub Action);
impl MenuAction for TabAction {
type Message = Message;
fn message(&self) -> Message {
Message::TabContextAction(self.0, self.1)
}
}
pub fn context_menu(
key_binds: &HashMap<KeyBind, Action>,
entity: segmented_button::Entity,
has_selection: bool,
) -> Element<'a, Message> {
fn key_style(theme: &cosmic::Theme) -> TextStyle {
// TODO use wayland popups
let mut color = theme.cosmic().background(false).component.on;
color.alpha *= 0.75;
TextStyle {
color: Some(color.into()),
..Default::default()
}
}
let base_menu_item = |menu_label, menu_action| {
let mut key = String::new();
for (key_bind, key_action) in key_binds.iter() {
if key_action == &menu_action {
key = key_bind.to_string();
break;
}
}
menu_button(vec![
widget::text(menu_label).into(),
space::horizontal().into(),
widget::text(key)
.class(theme::Text::Custom(key_style))
.into(),
])
};
let optional_menu_item = |menu_label, menu_action, disabled| {
base_menu_item(menu_label, menu_action).on_press_maybe(if disabled {
None
) -> Vec<widget::menu::Tree<Message>> {
let item =
|label: String, action: Action| MenuItem::Button(label, None, TabAction(entity, action));
let optional = |label: String, action: Action, enabled: bool| {
if enabled {
MenuItem::Button(label, None, TabAction(entity, action))
} else {
Some(Message::TabContextAction(entity, menu_action))
})
};
let menu_item = |menu_label, menu_action| {
base_menu_item(menu_label, menu_action)
.on_press(Message::TabContextAction(entity, menu_action))
};
widget::container(
cosmic::widget::menu::menu_column::MenuColumn::with_children([
menu_item(fl!("undo"), Action::Undo).into(),
menu_item(fl!("redo"), Action::Redo).into(),
divider::horizontal::light().into(),
optional_menu_item(fl!("cut"), Action::Cut, !has_selection).into(),
optional_menu_item(fl!("copy"), Action::Copy, !has_selection).into(),
menu_item(fl!("paste"), Action::Paste).into(),
menu_item(fl!("select-all"), Action::SelectAll).into(),
]),
)
.padding(1)
//TODO: move style to libcosmic
.style(|theme| {
let cosmic = theme.cosmic();
let component = &cosmic.background(false).component;
widget::container::Style {
icon_color: Some(component.on.into()),
text_color: Some(component.on.into()),
background: Some(Background::Color(component.base.into())),
border: Border {
radius: cosmic.radius_s().map(|x| x + 1.0).into(),
width: 1.0,
color: component.divider.into(),
},
..Default::default()
MenuItem::ButtonDisabled(label, None, TabAction(entity, action))
}
})
.width(Length::Fixed(240.0))
.into()
};
let key_binds: HashMap<KeyBind, TabAction> = key_binds
.iter()
.map(|(key_bind, action)| (key_bind.clone(), TabAction(entity, *action)))
.collect();
widget::menu::items(
&key_binds,
vec![
item(fl!("undo"), Action::Undo),
item(fl!("redo"), Action::Redo),
MenuItem::Divider,
optional(fl!("cut"), Action::Cut, has_selection),
optional(fl!("copy"), Action::Copy, has_selection),
item(fl!("paste"), Action::Paste),
item(fl!("select-all"), Action::SelectAll),
],
)
}
pub fn menu_bar<'a>(
@ -198,7 +167,7 @@ pub fn menu_bar<'a>(
config: &Config,
config_state: &ConfigState,
key_binds: &HashMap<KeyBind, Action>,
projects: &Vec<(String, PathBuf)>,
projects: &[(String, PathBuf)],
has_selection: bool,
) -> Element<'a, Message> {
//TODO: port to libcosmic

View file

@ -29,15 +29,15 @@ impl ProjectNode {
let path = fs::canonicalize(path)?;
let name = path
.file_name()
.ok_or(io::Error::new(
io::ErrorKind::Other,
format!("path {:?} has no file name", path),
))?
.ok_or(io::Error::other(format!(
"path {:?} has no file name",
path
)))?
.to_str()
.ok_or(io::Error::new(
io::ErrorKind::Other,
format!("path {:?} is not valid UTF-8", path),
))?
.ok_or(io::Error::other(format!(
"path {:?} is not valid UTF-8",
path
)))?
.to_string();
Ok(if path.is_dir() {
Self::Folder {

View file

@ -54,10 +54,10 @@ impl ProjectSearchResult {
}
};
if let Some(file_type) = entry.file_type() {
if file_type.is_dir() {
continue;
}
if let Some(file_type) = entry.file_type()
&& file_type.is_dir()
{
continue;
}
let entry_path = entry.path();

View file

@ -1,9 +1,6 @@
// SPDX-License-Identifier: GPL-3.0-only
use cosmic::{
iced::{Point, advanced::graphics::text::font_system},
widget::icon,
};
use cosmic::{iced::advanced::graphics::text::font_system, widget::icon};
use cosmic_files::mime_icon::{FALLBACK_MIME_ICON, mime_for_path, mime_icon};
use cosmic_text::{Attrs, Buffer, Cursor, Edit, Selection, Shaping, SyntaxEditor, ViEditor, Wrap};
use regex::Regex;
@ -28,6 +25,7 @@ fn editor_text(editor: &ViEditor<'static, 'static>) -> String {
})
}
#[allow(clippy::large_enum_variant)]
pub enum Tab {
Editor(EditorTab),
GitDiff(GitDiffTab),
@ -51,7 +49,7 @@ pub struct EditorTab {
pub path_opt: Option<PathBuf>,
attrs: Attrs<'static>,
pub editor: Mutex<ViEditor<'static, 'static>>,
pub context_menu: Option<Point>,
pub context_menu_open: bool,
pub zoom_adj: i8,
}
@ -75,7 +73,7 @@ impl EditorTab {
path_opt: None,
attrs,
editor: Mutex::new(ViEditor::new(editor)),
context_menu: None,
context_menu_open: false,
zoom_adj,
};
@ -316,7 +314,7 @@ impl EditorTab {
.filter_map(|m| {
if cursor.line != start_line
|| m.start() >= cursor.index
|| m.start() < cursor.index && wrapped == true
|| m.start() < cursor.index && wrapped
{
Some((m.start(), m.len()))
} else {
@ -393,7 +391,7 @@ impl EditorTab {
if cursor.line != start_line
|| m.start() > cursor.index
|| m.start() == cursor.index && current_selection == Selection::None
|| m.start() < cursor.index && wrapped == true
|| m.start() < cursor.index && wrapped
{
Some((m.start(), m.end()))
} else {
@ -436,7 +434,7 @@ impl EditorTab {
if cursor.line != start_line
|| m.start() < cursor.index
|| m.start() == cursor.index && current_selection == Selection::None
|| m.start() > cursor.index && wrapped == true
|| m.start() > cursor.index && wrapped
{
Some((m.start(), m.end()))
} else {

View file

@ -51,7 +51,7 @@ pub struct TextBox<'a, Message> {
on_focus: Option<Message>,
click_timing: Duration,
has_context_menu: bool,
on_context_menu: Option<Box<dyn Fn(Option<Point>) -> Message + 'a>>,
on_context_menu: Option<Box<dyn Fn(bool) -> Message + 'a>>,
highlight_current_line: bool,
line_numbers: bool,
}
@ -107,10 +107,7 @@ where
self
}
pub fn on_context_menu(
mut self,
on_context_menu: impl Fn(Option<Point>) -> Message + 'a,
) -> Self {
pub fn on_context_menu(mut self, on_context_menu: impl Fn(bool) -> Message + 'a) -> Self {
self.on_context_menu = Some(Box::new(on_context_menu));
self
}
@ -320,9 +317,8 @@ where
editor.with_buffer(|buffer| {
let mut layout_lines = 0;
for line in buffer.lines.iter() {
match line.layout_opt() {
Some(layout) => layout_lines += layout.len(),
None => (),
if let Some(layout) = line.layout_opt() {
layout_lines += layout.len()
}
}
@ -758,7 +754,7 @@ where
match editor.buffer_ref() {
cosmic_text::BufferRef::Arc(buffer) => {
renderer.fill_raw(Raw {
buffer: Arc::downgrade(&buffer),
buffer: Arc::downgrade(buffer),
position: pos,
color: Color::from_rgba(1.0, 1.0, 1.0, 1.0),
clip_bounds,
@ -1098,15 +1094,13 @@ where
editor.set_redraw(true);
shell.capture_event();
}
Named::Tab => {
if !modifiers.control() && !modifiers.alt() {
if modifiers.shift() {
editor.action(Action::Unindent);
} else {
editor.action(Action::Indent);
}
shell.capture_event();
Named::Tab if !modifiers.control() && !modifiers.alt() => {
if modifiers.shift() {
editor.action(Action::Unindent);
} else {
editor.action(Action::Indent);
}
shell.capture_event();
}
_ => (),
},
@ -1155,7 +1149,7 @@ where
InputMethodEvent::Commit(text) => {
if state.is_focused {
editor.start_change();
editor.insert_string(&text, None);
editor.insert_string(text, None);
editor.finish_change();
shell.capture_event();
}
@ -1177,13 +1171,13 @@ where
let y = y_logical * scale_factor;
// Do this first as the horizontal scrollbar is on top of the buffer
if let Some(scrollbar_h_rect) = state.scrollbar_h_rect.get() {
if scrollbar_h_rect.contains(Point::new(x_logical, y_logical)) {
state.dragging = Some(Dragging::ScrollbarH {
start_x: x,
start_scroll: editor.with_buffer(|buffer| buffer.scroll()),
});
}
if let Some(scrollbar_h_rect) = state.scrollbar_h_rect.get()
&& scrollbar_h_rect.contains(Point::new(x_logical, y_logical))
{
state.dragging = Some(Dragging::ScrollbarH {
start_x: x,
start_scroll: editor.with_buffer(|buffer| buffer.scroll()),
});
}
if matches!(state.dragging, Some(Dragging::ScrollbarH { .. })) {
@ -1210,7 +1204,7 @@ where
ClickKind::Single
};
let maybe_anchor = if state.modifiers.shift() {
state.shift_anchor.lock().unwrap().clone()
*state.shift_anchor.lock().unwrap()
} else {
None
};
@ -1267,14 +1261,9 @@ where
// Update context menu state
if let Some(on_context_menu) = &self.on_context_menu {
shell.publish((on_context_menu)(if self.has_context_menu {
None
} else {
match button {
Button::Right => Some(p),
_ => None,
}
}));
shell.publish((on_context_menu)(
!self.has_context_menu && matches!(button, Button::Right),
));
}
shell.capture_event();
@ -1396,11 +1385,9 @@ where
shell.capture_event();
}
}
Event::Window(WindowEvent::RedrawRequested(_now)) => {
if state.is_focused {
state.caret_position = editor.cursor_position().unwrap_or(state.caret_position);
shell.request_input_method(&self.input_method(state, scale_factor, layout));
}
Event::Window(WindowEvent::RedrawRequested(_now)) if state.is_focused => {
state.caret_position = editor.cursor_position().unwrap_or(state.caret_position);
shell.request_input_method(&self.input_method(state, scale_factor, layout));
}
_ => (),
}