add file I/O bridge between Swift and Iced editor via FFI text get/set
This commit is contained in:
parent
60723a6071
commit
768ec6db2c
|
|
@ -26,10 +26,15 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
var window: NSWindow!
|
var window: NSWindow!
|
||||||
var appState: AppState!
|
var appState: AppState!
|
||||||
private var titleCancellable: AnyCancellable?
|
private var titleCancellable: AnyCancellable?
|
||||||
|
private var textCancellable: AnyCancellable?
|
||||||
private var titleBarView: TitleBarView?
|
private var titleBarView: TitleBarView?
|
||||||
private var focusTitleObserver: NSObjectProtocol?
|
private var focusTitleObserver: NSObjectProtocol?
|
||||||
private var windowControllers: [WindowController] = []
|
private var windowControllers: [WindowController] = []
|
||||||
|
|
||||||
|
private var viewport: IcedViewportView? {
|
||||||
|
window?.contentView as? IcedViewportView
|
||||||
|
}
|
||||||
|
|
||||||
func applicationDidFinishLaunching(_ notification: Notification) {
|
func applicationDidFinishLaunching(_ notification: Notification) {
|
||||||
_ = ConfigManager.shared
|
_ = ConfigManager.shared
|
||||||
appState = AppState()
|
appState = AppState()
|
||||||
|
|
@ -57,6 +62,8 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
setupMenuBar()
|
setupMenuBar()
|
||||||
observeDocumentTitle()
|
observeDocumentTitle()
|
||||||
|
|
||||||
|
observeDocumentText()
|
||||||
|
|
||||||
DocumentBrowserController.shared = DocumentBrowserController(appState: appState)
|
DocumentBrowserController.shared = DocumentBrowserController(appState: appState)
|
||||||
|
|
||||||
NotificationCenter.default.addObserver(
|
NotificationCenter.default.addObserver(
|
||||||
|
|
@ -86,6 +93,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
func applicationWillTerminate(_ notification: Notification) {
|
func applicationWillTerminate(_ notification: Notification) {
|
||||||
|
syncTextFromViewport()
|
||||||
appState.saveNote()
|
appState.saveNote()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -301,6 +309,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func saveNote() {
|
@objc private func saveNote() {
|
||||||
|
syncTextFromViewport()
|
||||||
if appState.currentFileURL != nil {
|
if appState.currentFileURL != nil {
|
||||||
appState.saveNote()
|
appState.saveNote()
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -309,6 +318,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func saveNoteAs() {
|
@objc private func saveNoteAs() {
|
||||||
|
syncTextFromViewport()
|
||||||
let panel = NSSavePanel()
|
let panel = NSSavePanel()
|
||||||
panel.allowedContentTypes = Self.supportedContentTypes
|
panel.allowedContentTypes = Self.supportedContentTypes
|
||||||
panel.nameFieldStringValue = defaultFilename()
|
panel.nameFieldStringValue = defaultFilename()
|
||||||
|
|
@ -466,6 +476,22 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func observeDocumentText() {
|
||||||
|
textCancellable = appState.$documentText
|
||||||
|
.receive(on: RunLoop.main)
|
||||||
|
.sink { [weak self] text in
|
||||||
|
self?.viewport?.setText(text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func syncTextFromViewport() {
|
||||||
|
guard let vp = viewport else { return }
|
||||||
|
let text = vp.getText()
|
||||||
|
if !text.isEmpty || appState.documentText.isEmpty {
|
||||||
|
appState.documentText = text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func observeDocumentTitle() {
|
private func observeDocumentTitle() {
|
||||||
titleCancellable = appState.$documentText
|
titleCancellable = appState.$documentText
|
||||||
.receive(on: RunLoop.main)
|
.receive(on: RunLoop.main)
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import AppKit
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
class IcedViewportView: NSView {
|
class IcedViewportView: NSView {
|
||||||
private var viewportHandle: OpaquePointer?
|
private(set) var viewportHandle: OpaquePointer?
|
||||||
private var displayLink: CVDisplayLink?
|
private var displayLink: CVDisplayLink?
|
||||||
|
|
||||||
override init(frame frameRect: NSRect) {
|
override init(frame frameRect: NSRect) {
|
||||||
|
|
@ -159,7 +159,7 @@ class IcedViewportView: NSView {
|
||||||
|
|
||||||
if cmd && !shift {
|
if cmd && !shift {
|
||||||
switch chars {
|
switch chars {
|
||||||
case "a", "c", "v", "x", "z", "p", "t",
|
case "a", "b", "c", "i", "v", "x", "z", "p", "t",
|
||||||
"=", "+", "-", "0":
|
"=", "+", "-", "0":
|
||||||
keyDown(with: event)
|
keyDown(with: event)
|
||||||
return true
|
return true
|
||||||
|
|
@ -197,4 +197,21 @@ class IcedViewportView: NSView {
|
||||||
guard let h = viewportHandle else { return }
|
guard let h = viewportHandle else { return }
|
||||||
viewport_key_event(h, UInt32(event.keyCode), UInt32(event.modifierFlags.rawValue), true, nil)
|
viewport_key_event(h, UInt32(event.keyCode), UInt32(event.modifierFlags.rawValue), true, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Text Bridge
|
||||||
|
|
||||||
|
func setText(_ text: String) {
|
||||||
|
guard let h = viewportHandle else { return }
|
||||||
|
text.withCString { cstr in
|
||||||
|
viewport_set_text(h, cstr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getText() -> String {
|
||||||
|
guard let h = viewportHandle else { return "" }
|
||||||
|
guard let cstr = viewport_get_text(h) else { return "" }
|
||||||
|
let result = String(cString: cstr)
|
||||||
|
viewport_free_string(cstr)
|
||||||
|
return result
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -41,4 +41,12 @@ void viewport_scroll_event(struct ViewportHandle *handle,
|
||||||
float delta_x,
|
float delta_x,
|
||||||
float delta_y);
|
float delta_y);
|
||||||
|
|
||||||
|
void viewport_set_text(struct ViewportHandle *handle, const char *text);
|
||||||
|
|
||||||
|
void viewport_set_lang(struct ViewportHandle *handle, const char *ext);
|
||||||
|
|
||||||
|
char *viewport_get_text(struct ViewportHandle *handle);
|
||||||
|
|
||||||
|
void viewport_free_string(char *s);
|
||||||
|
|
||||||
#endif /* SWIFTLY_VIEWPORT_H */
|
#endif /* SWIFTLY_VIEWPORT_H */
|
||||||
|
|
|
||||||
|
|
@ -9,13 +9,20 @@ use iced_wgpu::core::{
|
||||||
use iced_widget::container;
|
use iced_widget::container;
|
||||||
use iced_widget::markdown;
|
use iced_widget::markdown;
|
||||||
use iced_widget::text_editor::{self, Binding, KeyPress, Motion, Status, Style};
|
use iced_widget::text_editor::{self, Binding, KeyPress, Motion, Status, Style};
|
||||||
|
use iced_wgpu::core::text::highlighter::Format;
|
||||||
|
use crate::syntax::{self, SyntaxHighlighter, SyntaxSettings};
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
#[allow(dead_code)]
|
||||||
pub enum Message {
|
pub enum Message {
|
||||||
EditorAction(text_editor::Action),
|
EditorAction(text_editor::Action),
|
||||||
TogglePreview,
|
TogglePreview,
|
||||||
MarkdownLink(markdown::Uri),
|
MarkdownLink(markdown::Uri),
|
||||||
InsertTable,
|
InsertTable,
|
||||||
|
ToggleBold,
|
||||||
|
ToggleItalic,
|
||||||
|
Evaluate,
|
||||||
|
SmartEval,
|
||||||
ZoomIn,
|
ZoomIn,
|
||||||
ZoomOut,
|
ZoomOut,
|
||||||
ZoomReset,
|
ZoomReset,
|
||||||
|
|
@ -26,6 +33,9 @@ pub struct EditorState {
|
||||||
pub font_size: f32,
|
pub font_size: f32,
|
||||||
pub preview: bool,
|
pub preview: bool,
|
||||||
pub parsed: Vec<markdown::Item>,
|
pub parsed: Vec<markdown::Item>,
|
||||||
|
pub eval_results: Vec<(usize, String)>,
|
||||||
|
pub eval_errors: Vec<(usize, String)>,
|
||||||
|
pub lang: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn md_style() -> markdown::Style {
|
fn md_style() -> markdown::Style {
|
||||||
|
|
@ -46,41 +56,90 @@ fn md_style() -> markdown::Style {
|
||||||
impl EditorState {
|
impl EditorState {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
let sample = concat!(
|
let sample = concat!(
|
||||||
"# Heading 1\n\n",
|
"use std::collections::HashMap;\n\n",
|
||||||
"## Heading 2\n\n",
|
"/// A simple key-value store.\n",
|
||||||
"### Heading 3\n\n",
|
"pub struct Store {\n",
|
||||||
"Regular text with **bold** and *italic* and `inline code`.\n\n",
|
" data: HashMap<String, i64>,\n",
|
||||||
"- Bullet one\n",
|
"}\n\n",
|
||||||
"- Bullet two\n",
|
"impl Store {\n",
|
||||||
"- Bullet three\n\n",
|
" pub fn new() -> Self {\n",
|
||||||
"1. First\n",
|
" Self { data: HashMap::new() }\n",
|
||||||
"2. Second\n",
|
" }\n\n",
|
||||||
"3. Third\n\n",
|
" pub fn insert(&mut self, key: &str, value: i64) {\n",
|
||||||
"> This is a blockquote\n\n",
|
" self.data.insert(key.to_string(), value);\n",
|
||||||
"```python\n",
|
" }\n\n",
|
||||||
"def hello():\n",
|
" pub fn get(&self, key: &str) -> Option<&i64> {\n",
|
||||||
" print(\"Hello world\")\n",
|
" self.data.get(key)\n",
|
||||||
"```\n\n",
|
" }\n",
|
||||||
"| Name | Age | City |\n",
|
"}\n\n",
|
||||||
"|------|-----|------|\n",
|
"fn main() {\n",
|
||||||
"| Alice | 30 | NYC |\n",
|
" let mut store = Store::new();\n",
|
||||||
"| Bob | 25 | LA |\n\n",
|
" store.insert(\"count\", 42);\n",
|
||||||
"---\n\n",
|
" if let Some(val) = store.get(\"count\") {\n",
|
||||||
"[Link text](https://example.com)\n",
|
" println!(\"value: {val}\");\n",
|
||||||
|
" }\n",
|
||||||
|
"}\n",
|
||||||
);
|
);
|
||||||
Self {
|
Self {
|
||||||
content: text_editor::Content::with_text(sample),
|
content: text_editor::Content::with_text(sample),
|
||||||
font_size: 14.0,
|
font_size: 14.0,
|
||||||
preview: false,
|
preview: false,
|
||||||
parsed: Vec::new(),
|
parsed: Vec::new(),
|
||||||
|
eval_results: Vec::new(),
|
||||||
|
eval_errors: Vec::new(),
|
||||||
|
lang: Some("rust".into()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_text(&mut self, text: &str) {
|
||||||
|
self.content = text_editor::Content::with_text(text);
|
||||||
|
self.reparse();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_lang_from_ext(&mut self, ext: &str) {
|
||||||
|
self.lang = lang_from_extension(ext);
|
||||||
|
}
|
||||||
|
|
||||||
fn reparse(&mut self) {
|
fn reparse(&mut self) {
|
||||||
let text = self.content.text();
|
let text = self.content.text();
|
||||||
self.parsed = markdown::parse(&text).collect();
|
self.parsed = markdown::parse(&text).collect();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn toggle_wrap(&mut self, marker: &str) {
|
||||||
|
let mlen = marker.len();
|
||||||
|
match self.content.selection() {
|
||||||
|
Some(sel) if sel.starts_with(marker) && sel.ends_with(marker) && sel.len() >= mlen * 2 => {
|
||||||
|
let inner = &sel[mlen..sel.len() - mlen];
|
||||||
|
self.content.perform(text_editor::Action::Edit(
|
||||||
|
text_editor::Edit::Paste(Arc::new(inner.to_string())),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Some(sel) => {
|
||||||
|
let wrapped = format!("{marker}{sel}{marker}");
|
||||||
|
self.content.perform(text_editor::Action::Edit(
|
||||||
|
text_editor::Edit::Paste(Arc::new(wrapped)),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
let empty = format!("{marker}{marker}");
|
||||||
|
self.content.perform(text_editor::Action::Edit(
|
||||||
|
text_editor::Edit::Paste(Arc::new(empty)),
|
||||||
|
));
|
||||||
|
for _ in 0..mlen {
|
||||||
|
self.content.perform(text_editor::Action::Move(Motion::Left));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.reparse();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_eval(&mut self) {
|
||||||
|
let text = self.content.text();
|
||||||
|
let doc = crate::eval::evaluate_document(&text);
|
||||||
|
self.eval_results = doc.results.into_iter().map(|r| (r.line, r.result)).collect();
|
||||||
|
self.eval_errors = doc.errors.into_iter().map(|e| (e.line, e.error)).collect();
|
||||||
|
}
|
||||||
|
|
||||||
pub fn update(&mut self, message: Message) {
|
pub fn update(&mut self, message: Message) {
|
||||||
match message {
|
match message {
|
||||||
Message::EditorAction(action) => {
|
Message::EditorAction(action) => {
|
||||||
|
|
@ -88,6 +147,7 @@ impl EditorState {
|
||||||
self.content.perform(action);
|
self.content.perform(action);
|
||||||
if is_edit {
|
if is_edit {
|
||||||
self.reparse();
|
self.reparse();
|
||||||
|
self.run_eval();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Message::InsertTable => {
|
Message::InsertTable => {
|
||||||
|
|
@ -96,6 +156,34 @@ impl EditorState {
|
||||||
text_editor::Edit::Paste(Arc::new(table.to_string())),
|
text_editor::Edit::Paste(Arc::new(table.to_string())),
|
||||||
));
|
));
|
||||||
self.reparse();
|
self.reparse();
|
||||||
|
self.run_eval();
|
||||||
|
}
|
||||||
|
Message::ToggleBold => {
|
||||||
|
self.toggle_wrap("**");
|
||||||
|
}
|
||||||
|
Message::ToggleItalic => {
|
||||||
|
self.toggle_wrap("*");
|
||||||
|
}
|
||||||
|
Message::Evaluate => {
|
||||||
|
self.run_eval();
|
||||||
|
}
|
||||||
|
Message::SmartEval => {
|
||||||
|
let cursor = self.content.cursor();
|
||||||
|
let text = self.content.text();
|
||||||
|
let lines: Vec<&str> = text.lines().collect();
|
||||||
|
let line_idx = cursor.position.line;
|
||||||
|
if line_idx < lines.len() {
|
||||||
|
let line = lines[line_idx].trim();
|
||||||
|
if let Some(varname) = parse_let_binding(line) {
|
||||||
|
let insert = format!("\n/= {varname}");
|
||||||
|
self.content.perform(text_editor::Action::Move(Motion::End));
|
||||||
|
self.content.perform(text_editor::Action::Edit(
|
||||||
|
text_editor::Edit::Paste(Arc::new(insert)),
|
||||||
|
));
|
||||||
|
self.reparse();
|
||||||
|
self.run_eval();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Message::TogglePreview => {
|
Message::TogglePreview => {
|
||||||
self.preview = !self.preview;
|
self.preview = !self.preview;
|
||||||
|
|
@ -140,7 +228,7 @@ impl EditorState {
|
||||||
})
|
})
|
||||||
.into()
|
.into()
|
||||||
} else {
|
} else {
|
||||||
iced_widget::text_editor(&self.content)
|
let editor = iced_widget::text_editor(&self.content)
|
||||||
.on_action(Message::EditorAction)
|
.on_action(Message::EditorAction)
|
||||||
.font(Font::MONOSPACE)
|
.font(Font::MONOSPACE)
|
||||||
.size(self.font_size)
|
.size(self.font_size)
|
||||||
|
|
@ -154,8 +242,25 @@ impl EditorState {
|
||||||
placeholder: Color::from_rgb(0.4, 0.4, 0.4),
|
placeholder: Color::from_rgb(0.4, 0.4, 0.4),
|
||||||
value: Color::WHITE,
|
value: Color::WHITE,
|
||||||
selection: Color::from_rgba(0.3, 0.5, 0.8, 0.4),
|
selection: Color::from_rgba(0.3, 0.5, 0.8, 0.4),
|
||||||
})
|
});
|
||||||
.into()
|
|
||||||
|
if let Some(lang) = &self.lang {
|
||||||
|
let settings = SyntaxSettings {
|
||||||
|
lang: lang.clone(),
|
||||||
|
source: self.content.text(),
|
||||||
|
};
|
||||||
|
editor
|
||||||
|
.highlight_with::<SyntaxHighlighter>(
|
||||||
|
settings,
|
||||||
|
|highlight, _theme| Format {
|
||||||
|
color: Some(syntax::highlight_color(highlight.kind)),
|
||||||
|
font: None,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.into()
|
||||||
|
} else {
|
||||||
|
editor.into()
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let mode_label = if self.preview { "Preview" } else { "Edit" };
|
let mode_label = if self.preview { "Preview" } else { "Edit" };
|
||||||
|
|
@ -182,12 +287,73 @@ impl EditorState {
|
||||||
snap: false,
|
snap: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
iced_widget::column([main_content, status_bar.into()])
|
let mut col_items: Vec<Element<'_, Message, Theme, iced_wgpu::Renderer>> =
|
||||||
|
vec![main_content];
|
||||||
|
|
||||||
|
if !self.eval_results.is_empty() || !self.eval_errors.is_empty() {
|
||||||
|
let mut result_items: Vec<Element<'_, Message, Theme, iced_wgpu::Renderer>> = Vec::new();
|
||||||
|
|
||||||
|
for (ln, val) in &self.eval_results {
|
||||||
|
result_items.push(
|
||||||
|
iced_widget::text(format!("Ln {}: {}", ln + 1, val))
|
||||||
|
.font(Font::MONOSPACE)
|
||||||
|
.size(11.0)
|
||||||
|
.color(Color::from_rgb(0.651, 0.890, 0.631))
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
for (ln, err) in &self.eval_errors {
|
||||||
|
result_items.push(
|
||||||
|
iced_widget::text(format!("Ln {}: {}", ln + 1, err))
|
||||||
|
.font(Font::MONOSPACE)
|
||||||
|
.size(11.0)
|
||||||
|
.color(Color::from_rgb(0.890, 0.400, 0.400))
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let eval_panel = iced_widget::container(
|
||||||
|
iced_widget::column(result_items).spacing(2.0),
|
||||||
|
)
|
||||||
|
.width(Length::Fill)
|
||||||
|
.padding(Padding { top: 4.0, right: 10.0, bottom: 4.0, left: 10.0 })
|
||||||
|
.style(|_theme: &Theme| container::Style {
|
||||||
|
background: Some(Background::Color(Color::from_rgb(0.10, 0.10, 0.12))),
|
||||||
|
border: Border::default(),
|
||||||
|
text_color: None,
|
||||||
|
shadow: Shadow::default(),
|
||||||
|
snap: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
col_items.push(eval_panel.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
col_items.push(status_bar.into());
|
||||||
|
|
||||||
|
iced_widget::column(col_items)
|
||||||
.height(Length::Fill)
|
.height(Length::Fill)
|
||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn parse_let_binding(line: &str) -> Option<String> {
|
||||||
|
let rest = line.strip_prefix("let ")?;
|
||||||
|
let eq_pos = rest.find('=')?;
|
||||||
|
if rest.as_bytes().get(eq_pos + 1) == Some(&b'=') {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let name_part = rest[..eq_pos].trim();
|
||||||
|
let name = if let Some(colon) = name_part.find(':') {
|
||||||
|
name_part[..colon].trim()
|
||||||
|
} else {
|
||||||
|
name_part
|
||||||
|
};
|
||||||
|
if name.is_empty() || !name.chars().all(|c| c.is_alphanumeric() || c == '_') {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(name.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
fn macos_key_binding(key_press: KeyPress) -> Option<Binding<Message>> {
|
fn macos_key_binding(key_press: KeyPress) -> Option<Binding<Message>> {
|
||||||
let KeyPress { key, modifiers, status, .. } = &key_press;
|
let KeyPress { key, modifiers, status, .. } = &key_press;
|
||||||
|
|
||||||
|
|
@ -232,3 +398,35 @@ fn macos_key_binding(key_press: KeyPress) -> Option<Binding<Message>> {
|
||||||
_ => Binding::from_key_press(key_press),
|
_ => Binding::from_key_press(key_press),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn lang_from_extension(ext: &str) -> Option<String> {
|
||||||
|
let lang = match ext {
|
||||||
|
"rs" => "rust",
|
||||||
|
"c" | "h" => "c",
|
||||||
|
"cpp" | "cc" | "cxx" | "hpp" | "hxx" => "cpp",
|
||||||
|
"js" | "mjs" | "cjs" => "javascript",
|
||||||
|
"jsx" => "jsx",
|
||||||
|
"ts" | "mts" | "cts" => "typescript",
|
||||||
|
"tsx" => "tsx",
|
||||||
|
"py" => "python",
|
||||||
|
"go" => "go",
|
||||||
|
"rb" => "ruby",
|
||||||
|
"sh" | "bash" | "zsh" => "bash",
|
||||||
|
"java" => "java",
|
||||||
|
"html" | "htm" => "html",
|
||||||
|
"css" => "css",
|
||||||
|
"scss" => "scss",
|
||||||
|
"less" => "less",
|
||||||
|
"json" => "json",
|
||||||
|
"lua" => "lua",
|
||||||
|
"php" => "php",
|
||||||
|
"toml" => "toml",
|
||||||
|
"yaml" | "yml" => "yaml",
|
||||||
|
"swift" => "swift",
|
||||||
|
"zig" => "zig",
|
||||||
|
"sql" => "sql",
|
||||||
|
"mk" => "make",
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
Some(lang.to_string())
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -168,6 +168,9 @@ pub fn render(handle: &mut ViewportHandle) {
|
||||||
match c.as_str() {
|
match c.as_str() {
|
||||||
"p" => messages.push(Message::TogglePreview),
|
"p" => messages.push(Message::TogglePreview),
|
||||||
"t" => messages.push(Message::InsertTable),
|
"t" => messages.push(Message::InsertTable),
|
||||||
|
"b" => messages.push(Message::ToggleBold),
|
||||||
|
"i" => messages.push(Message::ToggleItalic),
|
||||||
|
"e" => messages.push(Message::SmartEval),
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
use std::ffi::{c_char, c_void};
|
use std::ffi::{c_char, c_void, CStr, CString};
|
||||||
|
|
||||||
mod bridge;
|
mod bridge;
|
||||||
mod editor;
|
mod editor;
|
||||||
mod handle;
|
mod handle;
|
||||||
|
mod syntax;
|
||||||
|
|
||||||
pub use swiftly_core::*;
|
pub use swiftly_core::*;
|
||||||
|
|
||||||
|
|
@ -127,3 +128,47 @@ pub extern "C" fn viewport_scroll_event(
|
||||||
};
|
};
|
||||||
bridge::push_scroll_event(h, x, y, delta_x, delta_y);
|
bridge::push_scroll_event(h, x, y, delta_x, delta_y);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub extern "C" fn viewport_set_text(handle: *mut ViewportHandle, text: *const c_char) {
|
||||||
|
let h = match unsafe { handle.as_mut() } {
|
||||||
|
Some(h) => h,
|
||||||
|
None => return,
|
||||||
|
};
|
||||||
|
let s = if text.is_null() {
|
||||||
|
""
|
||||||
|
} else {
|
||||||
|
unsafe { CStr::from_ptr(text) }.to_str().unwrap_or("")
|
||||||
|
};
|
||||||
|
h.state.set_text(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub extern "C" fn viewport_set_lang(handle: *mut ViewportHandle, ext: *const c_char) {
|
||||||
|
let h = match unsafe { handle.as_mut() } {
|
||||||
|
Some(h) => h,
|
||||||
|
None => return,
|
||||||
|
};
|
||||||
|
if ext.is_null() {
|
||||||
|
h.state.lang = None;
|
||||||
|
} else {
|
||||||
|
let s = unsafe { CStr::from_ptr(ext) }.to_str().unwrap_or("");
|
||||||
|
h.state.set_lang_from_ext(s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub extern "C" fn viewport_get_text(handle: *mut ViewportHandle) -> *mut c_char {
|
||||||
|
let h = match unsafe { handle.as_mut() } {
|
||||||
|
Some(h) => h,
|
||||||
|
None => return std::ptr::null_mut(),
|
||||||
|
};
|
||||||
|
let text = h.state.content.text();
|
||||||
|
CString::new(text).unwrap_or_default().into_raw()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub extern "C" fn viewport_free_string(s: *mut c_char) {
|
||||||
|
if s.is_null() { return; }
|
||||||
|
unsafe { drop(CString::from_raw(s)); }
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,125 @@
|
||||||
|
use std::ops::Range;
|
||||||
|
|
||||||
|
use iced_wgpu::core::text::highlighter;
|
||||||
|
use iced_wgpu::core::Color;
|
||||||
|
use swiftly_core::highlight::{highlight_source, HighlightSpan};
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq)]
|
||||||
|
pub struct SyntaxSettings {
|
||||||
|
pub lang: String,
|
||||||
|
pub source: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub struct SyntaxHighlight {
|
||||||
|
pub kind: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct SyntaxHighlighter {
|
||||||
|
lang: String,
|
||||||
|
spans: Vec<HighlightSpan>,
|
||||||
|
line_offsets: Vec<usize>,
|
||||||
|
current_line: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SyntaxHighlighter {
|
||||||
|
fn rebuild(&mut self, source: &str) {
|
||||||
|
self.spans = highlight_source(source, &self.lang);
|
||||||
|
self.line_offsets.clear();
|
||||||
|
let mut offset = 0;
|
||||||
|
for line in source.split('\n') {
|
||||||
|
self.line_offsets.push(offset);
|
||||||
|
offset += line.len() + 1;
|
||||||
|
}
|
||||||
|
self.current_line = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl highlighter::Highlighter for SyntaxHighlighter {
|
||||||
|
type Settings = SyntaxSettings;
|
||||||
|
type Highlight = SyntaxHighlight;
|
||||||
|
type Iterator<'a> = std::vec::IntoIter<(Range<usize>, SyntaxHighlight)>;
|
||||||
|
|
||||||
|
fn new(settings: &Self::Settings) -> Self {
|
||||||
|
let mut h = SyntaxHighlighter {
|
||||||
|
lang: settings.lang.clone(),
|
||||||
|
spans: Vec::new(),
|
||||||
|
line_offsets: Vec::new(),
|
||||||
|
current_line: 0,
|
||||||
|
};
|
||||||
|
h.rebuild(&settings.source);
|
||||||
|
h
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(&mut self, new_settings: &Self::Settings) {
|
||||||
|
self.lang = new_settings.lang.clone();
|
||||||
|
self.rebuild(&new_settings.source);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn change_line(&mut self, line: usize) {
|
||||||
|
self.current_line = self.current_line.min(line);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn highlight_line(&mut self, _line: &str) -> Self::Iterator<'_> {
|
||||||
|
let ln = self.current_line;
|
||||||
|
self.current_line += 1;
|
||||||
|
|
||||||
|
if ln >= self.line_offsets.len() {
|
||||||
|
return Vec::new().into_iter();
|
||||||
|
}
|
||||||
|
|
||||||
|
let line_start = self.line_offsets[ln];
|
||||||
|
let line_end = if ln + 1 < self.line_offsets.len() {
|
||||||
|
self.line_offsets[ln + 1] - 1
|
||||||
|
} else {
|
||||||
|
line_start + _line.len()
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut result = Vec::new();
|
||||||
|
for span in &self.spans {
|
||||||
|
if span.end <= line_start || span.start >= line_end {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let start = span.start.max(line_start) - line_start;
|
||||||
|
let end = span.end.min(line_end) - line_start;
|
||||||
|
if start < end {
|
||||||
|
result.push((start..end, SyntaxHighlight { kind: span.kind }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result.into_iter()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn current_line(&self) -> usize {
|
||||||
|
self.current_line
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn highlight_color(kind: u8) -> Color {
|
||||||
|
match kind {
|
||||||
|
0 => Color::from_rgb(0.804, 0.569, 0.945), // keyword - mauve
|
||||||
|
1 => Color::from_rgb(0.537, 0.706, 0.980), // function - blue
|
||||||
|
2 => Color::from_rgb(0.604, 0.831, 0.898), // function.builtin - teal
|
||||||
|
3 => Color::from_rgb(0.976, 0.827, 0.522), // type - yellow
|
||||||
|
4 => Color::from_rgb(0.976, 0.827, 0.522), // type.builtin - yellow
|
||||||
|
5 => Color::from_rgb(0.569, 0.878, 0.800), // constructor - teal
|
||||||
|
6 => Color::from_rgb(0.988, 0.702, 0.529), // constant - peach
|
||||||
|
7 => Color::from_rgb(0.988, 0.702, 0.529), // constant.builtin - peach
|
||||||
|
8 => Color::from_rgb(0.651, 0.890, 0.631), // string - green
|
||||||
|
9 => Color::from_rgb(0.988, 0.702, 0.529), // number - peach
|
||||||
|
10 => Color::from_rgb(0.424, 0.443, 0.537), // comment - overlay0
|
||||||
|
11 => Color::from_rgb(0.804, 0.839, 0.957), // variable - text
|
||||||
|
12 => Color::from_rgb(0.949, 0.604, 0.584), // variable.builtin - red
|
||||||
|
13 => Color::from_rgb(0.949, 0.773, 0.584), // variable.parameter - flamingo
|
||||||
|
14 => Color::from_rgb(0.604, 0.831, 0.898), // operator - sky
|
||||||
|
15 => Color::from_rgb(0.580, 0.612, 0.733), // punctuation - overlay2
|
||||||
|
16 => Color::from_rgb(0.580, 0.612, 0.733), // punctuation.bracket - overlay2
|
||||||
|
17 => Color::from_rgb(0.580, 0.612, 0.733), // punctuation.delimiter - overlay2
|
||||||
|
18 => Color::from_rgb(0.537, 0.706, 0.980), // property - blue
|
||||||
|
19 => Color::from_rgb(0.804, 0.569, 0.945), // tag - mauve
|
||||||
|
20 => Color::from_rgb(0.976, 0.827, 0.522), // attribute - yellow
|
||||||
|
21 => Color::from_rgb(0.569, 0.878, 0.800), // label - teal
|
||||||
|
22 => Color::from_rgb(0.949, 0.604, 0.584), // escape - red
|
||||||
|
23 => Color::from_rgb(0.804, 0.839, 0.957), // embedded - text
|
||||||
|
_ => Color::from_rgb(0.804, 0.839, 0.957), // default text
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue