Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ On Windows, build from **x64 Native Tools Command Prompt for VS 2022** to avoid

```
src/ # Desktop app — main binary, GPUI views, app coordinator
crates/ # Library crates (29 crates, see below)
crates/ # Library crates (30 crates, see below)
mobile/ # Mobile app — React Native UI (mobile/rn) over the Rust core via uniffi (crates/okena-mobile-ffi)
web/ # Web client (React + TypeScript + xterm.js)
assets/ # Fonts, icons (assets/icons/*.svg referenced as icons/*.svg)
Expand All @@ -42,6 +42,7 @@ Most logic lives in `crates/`. The `src/` modules are thin re-exports (`pub use
| `okena-theme` | Theming system (built-in + custom themes) |
| `okena-ui` | Design tokens, shared UI utilities |
| `okena-files` | File search, file viewer, syntax highlighting |
| `okena-highlight` | syntect/tree-sitter syntax highlighting shared by the file viewer, diff viewer and markdown code blocks |
| `okena-markdown` | Markdown parsing and rendering |
| `okena-views-terminal` | Terminal pane, layout container, split/tabs views |
| `okena-views-sidebar` | Sidebar, project list, folder list, drag-and-drop |
Expand Down
18 changes: 14 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[workspace]
members = [".", "crates/okena-mobile-ffi", "crates/okena-core", "crates/okena-transport", "crates/okena-git", "crates/okena-views-git", "crates/okena-views-services", "crates/okena-views-sidebar", "crates/okena-views-terminal", "crates/okena-terminal", "crates/okena-layout", "crates/okena-state", "crates/okena-hooks", "crates/okena-workspace", "crates/okena-ui", "crates/okena-usage", "crates/okena-files", "crates/okena-markdown", "crates/okena-extensions", "crates/okena-ext-claude", "crates/okena-ext-codex", "crates/okena-ext-github", "crates/okena-ext-updater", "crates/okena-services", "crates/okena-remote-client", "crates/okena-remote-server", "crates/okena-views-remote", "crates/okena-theme", "crates/okena-cli", "crates/okena-app-core", "crates/okena-app", "crates/okena-daemon-core", "crates/okena-daemon", "crates/okena-tui"]
members = [".", "crates/okena-mobile-ffi", "crates/okena-core", "crates/okena-transport", "crates/okena-git", "crates/okena-views-git", "crates/okena-views-services", "crates/okena-views-sidebar", "crates/okena-views-terminal", "crates/okena-terminal", "crates/okena-layout", "crates/okena-state", "crates/okena-hooks", "crates/okena-workspace", "crates/okena-ui", "crates/okena-usage", "crates/okena-files", "crates/okena-markdown", "crates/okena-highlight", "crates/okena-extensions", "crates/okena-ext-claude", "crates/okena-ext-codex", "crates/okena-ext-github", "crates/okena-ext-updater", "crates/okena-services", "crates/okena-remote-client", "crates/okena-remote-server", "crates/okena-views-remote", "crates/okena-theme", "crates/okena-cli", "crates/okena-app-core", "crates/okena-app", "crates/okena-daemon-core", "crates/okena-daemon", "crates/okena-tui"]
resolver = "2"

[workspace.package]
Expand Down
16 changes: 4 additions & 12 deletions crates/okena-files/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,8 @@ gpui = [
"dep:gpui-component",
"dep:okena-ui",
"dep:okena-markdown",
"dep:okena-highlight",
"dep:syntect",
"dep:two-face",
"dep:tree-sitter",
"dep:tree-sitter-md",
"dep:streaming-iterator",
"dep:usvg",
"dep:ttf-parser",
"dep:image",
Expand All @@ -25,20 +22,15 @@ gpui = [
okena-core = { path = "../okena-core" }
okena-transport = { path = "../okena-transport", features = ["cancellable-http"] }
okena-markdown = { path = "../okena-markdown", optional = true }
okena-highlight = { path = "../okena-highlight", optional = true }
okena-ui = { path = "../okena-ui", optional = true }

gpui = { git = "https://github.com/zed-industries/zed", package = "gpui", optional = true }
gpui-component = { git = "https://github.com/longbridge/gpui-component", package = "gpui-component", optional = true }

# The highlighting itself lives in okena-highlight; syntect stays here for the
# `SyntaxSet` the viewers hold and pass back in.
syntect = { version = "5", features = ["default-fancy"], optional = true }
two-face = { version = "0.5", default-features = false, features = ["syntect-fancy"], optional = true }

# Markdown highlighting: tree-sitter is ~20x faster than syntect's Markdown
# grammar on large files. tree-sitter (0.26) and streaming-iterator are already
# in the workspace via gpui-component, so this only adds the grammar crate.
tree-sitter = { version = "0.26", optional = true }
tree-sitter-md = { version = "0.5", features = ["parser"], optional = true }
streaming-iterator = { version = "0.1", optional = true }

grep-searcher = "0.1"
grep-regex = "0.1"
Expand Down
89 changes: 5 additions & 84 deletions crates/okena-files/src/code_view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
//! - Text selection utilities

use crate::selection::SelectionState;
use crate::syntax::{HighlightedLine, HighlightedSpan};
use crate::syntax::HighlightedLine;
use gpui::*;

/// Type alias for code selection (line index, column).
Expand Down Expand Up @@ -93,89 +93,10 @@ pub fn update_scrollbar_drag(
.set_offset(point(px(0.0), px(-new_scroll)));
}

/// Build a StyledText with optional background highlights (e.g. selection or word-level diff).
/// Splits syntax color highlights at background range boundaries to produce
/// non-overlapping highlights (required by `StyledText::compute_runs`).
pub fn build_styled_text_with_backgrounds(
spans: &[HighlightedSpan],
bg_ranges: &[(std::ops::Range<usize>, Hsla)],
) -> StyledText {
let mut text = String::new();
let mut highlights = Vec::new();

for span in spans {
text.push_str(&span.text);
}

if bg_ranges.is_empty() {
// Fast path: no background highlights, just syntax colors
let mut offset = 0;
for span in spans {
let start = offset;
offset += span.text.len();
if start < offset {
highlights.push((
start..offset,
HighlightStyle {
color: Some(span.color.into()),
..Default::default()
},
));
}
}
} else {
// Split syntax spans at background range boundaries so no highlights overlap
let mut offset = 0;
for span in spans {
let span_start = offset;
let span_end = offset + span.text.len();
offset = span_end;

if span_start >= span_end {
continue;
}

// Collect boundary points from bg_ranges that fall within this span
let mut boundaries = vec![span_start];
for (br, _) in bg_ranges {
if br.start > span_start && br.start < span_end {
boundaries.push(br.start);
}
if br.end > span_start && br.end < span_end {
boundaries.push(br.end);
}
}
boundaries.push(span_end);
boundaries.sort();
boundaries.dedup();

for window in boundaries.windows(2) {
let sub_start = window[0];
let sub_end = window[1];
if sub_start >= sub_end {
continue;
}

let mut style = HighlightStyle {
color: Some(span.color.into()),
..Default::default()
};

// Apply background if this sub-range falls within any background range
for (br, bg_color) in bg_ranges {
if sub_start >= br.start && sub_end <= br.end {
style.background_color = Some(*bg_color);
break;
}
}

highlights.push((sub_start..sub_end, style));
}
}
}

StyledText::new(text).with_highlights(highlights)
}
// Lives in `okena-highlight` (shared with the markdown renderer); re-exported so
// the code viewers keep their `code_view::build_styled_text_with_backgrounds`
// import.
pub use okena_highlight::styled::build_styled_text_with_backgrounds;

/// Compute selection background ranges for a single line.
///
Expand Down
4 changes: 3 additions & 1 deletion crates/okena-files/src/file_viewer/loading.rs
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,9 @@ impl FileViewerTab {
self.content = content;
self.do_highlight_content(&self.file_path.clone(), syntax_set, is_dark);
if self.is_markdown {
self.markdown_doc = Some(MarkdownDocument::parse(&self.content));
let mut doc = MarkdownDocument::parse(&self.content);
doc.highlight_code_blocks(is_dark);
self.markdown_doc = Some(doc);
}
}
Ok(LoadedContent::Image { decoded, source }) => {
Expand Down
5 changes: 5 additions & 0 deletions crates/okena-files/src/file_viewer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -980,6 +980,11 @@ impl FileViewer {
// source-view XML), so they need the rehighlight too.
if rehighlight && !tab.is_font && (!tab.is_image || tab.is_svg) {
tab.do_highlight_content(&tab.file_path.clone(), &self.syntax_set, self.is_dark);
// The rendered markdown view carries its own highlighted code
// blocks, separate from the source view's lines.
if let Some(doc) = tab.markdown_doc.as_mut() {
doc.highlight_code_blocks(self.is_dark);
}
}
}
}
Expand Down
6 changes: 3 additions & 3 deletions crates/okena-files/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,11 @@ pub mod in_page_search;
#[cfg(feature = "gpui")]
pub mod list_overlay;
#[cfg(feature = "gpui")]
pub mod markdown_highlight;
#[cfg(feature = "gpui")]
pub mod selection;
// Highlighting lives in `okena-highlight` (shared with the markdown renderer);
// re-exported here so the viewers keep their `okena_files::syntax` paths.
#[cfg(feature = "gpui")]
pub mod syntax;
pub use okena_highlight::{markdown_highlight, syntax};
// `theme` re-exports gpui theme helpers from `okena-ui` (a gpui crate) and is
// consumed only by gpui code, so it is gated with the rest of the viewer even
// though it holds no rendering itself.
Expand Down
18 changes: 18 additions & 0 deletions crates/okena-highlight/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[package]
name = "okena-highlight"
version = "0.1.0"
edition = "2024"
license = "MIT"

[dependencies]
gpui = { git = "https://github.com/zed-industries/zed", package = "gpui" }

syntect = { version = "5", features = ["default-fancy"] }
two-face = { version = "0.5", default-features = false, features = ["syntect-fancy"] }

# Markdown highlighting: tree-sitter is ~20x faster than syntect's Markdown
# grammar on large files. tree-sitter (0.26) and streaming-iterator are already
# in the workspace via gpui-component, so this only adds the grammar crate.
tree-sitter = "0.26"
tree-sitter-md = { version = "0.5", features = ["parser"] }
streaming-iterator = "0.1"
11 changes: 11 additions & 0 deletions crates/okena-highlight/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#![cfg_attr(not(test), warn(clippy::unwrap_used, clippy::expect_used))]

//! Syntax highlighting shared by every viewer that shows code: the file viewer,
//! the diff viewer, and markdown code blocks.
//!
//! It sits below `okena-files` and `okena-markdown` so all three can share one
//! `SyntaxSet` — loading it is expensive and a second copy would be megabytes.

pub mod markdown_highlight;
pub mod styled;
pub mod syntax;
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

use crate::syntax::{
HighlightedLine, HighlightedSpan, default_text_color, highlight_line, load_syntax_theme,
map_extension_to_syntax,
syntax_for_language,
};
use gpui::Rgba;
use std::collections::HashMap;
Expand Down Expand Up @@ -229,7 +229,7 @@ fn collect_code_blocks(
let Some(code) = content.get(start..end.min(content.len())) else {
continue;
};
let syntax = syntax_for_lang(lang, syntax_set);
let syntax = syntax_for_language(lang, syntax_set);
let mut highlighter = HighlightLines::new(syntax, theme);
let first_line = line_of(line_starts, start);
for (i, line) in LinesWithEndings::from(code).enumerate() {
Expand All @@ -239,18 +239,6 @@ fn collect_code_blocks(
}
}

/// Resolve a fenced-code info-string language to a syntect syntax, falling back
/// to plain text. Mirrors [`crate::syntax::get_syntax_for_path`]'s mapping.
fn syntax_for_lang<'a>(
lang: &str,
syntax_set: &'a SyntaxSet,
) -> &'a syntect::parsing::SyntaxReference {
map_extension_to_syntax(lang)
.and_then(|mapped| syntax_set.find_syntax_by_extension(mapped))
.or_else(|| syntax_set.find_syntax_by_token(lang))
.unwrap_or_else(|| syntax_set.find_syntax_plain_text())
}

/// Build spans for one (non-code) line from the per-byte colour buffer,
/// coalescing equal-coloured runs, expanding tabs, and dropping line endings.
fn line_spans(
Expand Down
Loading
Loading