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
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,12 @@ class AndroidFeedbackRecordingTest(private val scenario: String) {
val effect = envelope.optJSONObject("effect") ?: return
envelope.optString("patch").takeIf { it.isNotEmpty() }?.let { patch -> ui.runOnIdle { filter.applyJson(patch) } }
val kind = effect.optString("kind")
var message = ""
if (kind !in setOf("refresh-graphs", "persist-ui-session", "persist-composer-draft", "save-settings", "present-attachment")) {
apply(request("executeEffect", effect.toString()))
val response = request("executeEffect", effect.toString())
if (kind == "maintain-search-index") message = response else apply(response)
}
ui.runOnIdle { bridge.resolveEffect(effect.getLong("id"), true, "").takeIf { it.isNotEmpty() }?.let(filter::applyJson) }
ui.runOnIdle { bridge.resolveEffect(effect.getLong("id"), true, message).takeIf { it.isNotEmpty() }?.let(filter::applyJson) }
}
error("The native effect queue did not drain")
}
Expand Down Expand Up @@ -105,7 +107,9 @@ class AndroidFeedbackRecordingTest(private val scenario: String) {
val selected = request("selectPage", uuid(page))
selectedSnapshot = selected
val root = bridge.initialize(authenticationCode = 3)
apply(opened); apply(selected); host("graph-loading", "false")
apply(opened); apply(selected)
host("local-graph-ids", """["feedback"]""")
host("graph-loading", "false")
ui.setContent { MaterialExpressiveTheme(colorScheme = LogseqTheme.lightScheme, typography = LogseqTheme.typography) {
Box(Modifier.fillMaxSize()) { backend.Content(root) }
} }
Expand Down Expand Up @@ -212,6 +216,44 @@ class AndroidFeedbackRecordingTest(private val scenario: String) {
ui.onNodeWithTag("button.outliner.code-language.option.python").performClick(); pause()
}
}
"calculator-live" -> {
mount(2)
val configured = request("nodeProperty", JSONObject().put("action", "setCodeLanguage")
.put("blockUuid", uuid(12)).put("value", "calc").toString())
assertTrue(configured, JSONObject(configured).optBoolean("ok"))
apply(configured)
pause()
val content = ui.onNodeWithTag("outliner.code.content." + uuid(12), useUnmergedTree = true)
content.assertExists(ui.onRoot(useUnmergedTree = true).printToString())
content.assertIsDisplayed().performClick()
pause()
val editor = ui.onNodeWithTag("field.outliner.block." + uuid(12), useUnmergedTree = true)
editor.assertExists(ui.onRoot(useUnmergedTree = true).printToString())
val results = ui.onNodeWithTag("outliner.calculator.results." + uuid(12), useUnmergedTree = true)
editor.performTextReplacement("6*7\nlast*3")
ui.waitUntil(10_000) {
drain()
runCatching { results.assertTextEquals("42\n126") }.isSuccess
}
val inputBounds = editor.getUnclippedBoundsInRoot()
val outputBounds = results.getUnclippedBoundsInRoot()
assertTrue("Calculator results occupy the right column", outputBounds.left >= inputBounds.right)
editor.performImeAction()
ui.waitUntil(10_000) {
drain()
runCatching { editor.assertDoesNotExist() }.isSuccess
}
val after = JSONObject(request("refresh")).getJSONObject("result").getJSONArray("outlinerRows")
val beforeCount = JSONObject(configured).getJSONObject("result").getJSONArray("outlinerRows").length()
assertEquals("Enter creates exactly one new block", beforeCount + 1, after.length())
results.assertTextEquals("42\n126")
content.performTouchInput { longClick() }
ui.waitUntil(10_000) {
drain()
runCatching { ui.onNodeWithTag("button.outliner.selection.copy").assertIsDisplayed() }.isSuccess
}
pause()
}
"language" -> {
mount(1); host("settings", """{"appearance":"system","language":"system","systemLanguage":"zh-CN","spellCheck":true,"autoCorrection":true,"sidebarTabs":[],"baseURL":"http://127.0.0.1:1","version":"QA","revision":"feedback"}""")
ui.onNodeWithTag("button.sidebar").performClick(); pause()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import androidx.compose.ui.test.click
import androidx.compose.ui.test.longClick
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onAllNodesWithContentDescription
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performSemanticsAction
import androidx.compose.ui.test.performTouchInput
Expand Down Expand Up @@ -106,6 +108,21 @@ class AndroidRichContentTest {
@Test fun quoteLongPressSelectsInsteadOfEditing() =
assertLongPressSelects("""[{"type":"quote","children":[{"type":"text","text":"Quote block"}]}]""", "Quote block")

@Test fun codeLongPressSelectsInsteadOfEditing() =
assertLongPressSelects("""[{"type":"codeBlock","style":"python","text":"print(42)"}]""", "print(42)")

@Test fun mathLongPressSelectsInsteadOfEditing() {
render("""[{"type":"math","style":"display","text":"x^2"}]""")
ui.waitUntil(10_000) {
ui.onAllNodesWithContentDescription("x^2").fetchSemanticsNodes().isNotEmpty()
}
ui.onNodeWithContentDescription("x^2").performTouchInput { longClick() }
ui.runOnIdle {
assertEquals("A formula long press selects the block exactly once", listOf("drag-start"),
events.filterIsInstance<LuiEvent.Extension>().map { it.name })
}
}

@Test fun malformedBodyLongPressStillSelects() = assertLongPressSelects("not-json", "Readable fallback")

@Test fun shortBodyTapStillEditsExactlyOnce() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
Expand All @@ -36,6 +37,8 @@ import dev.snipme.highlights.model.SyntaxThemes
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext

internal val LocalShowCodeLanguage = staticCompositionLocalOf { true }

private data class CodeKey(val code: String, val language: String, val dark: Boolean, val background: Int)
private val codeCache = object : LruCache<CodeKey, AnnotatedString>(512 * 1024) {
override fun sizeOf(key: CodeKey, value: AnnotatedString) =
Expand Down Expand Up @@ -82,11 +85,11 @@ internal fun AndroidCodeBlock(code: String, language: String) {
}
Surface(
shape = RoundedCornerShape(8.dp),
color = MaterialTheme.colorScheme.surfaceContainerHigh,
color = if (LocalShowCodeLanguage.current) MaterialTheme.colorScheme.surfaceContainerHigh else Color.Transparent,
modifier = Modifier.fillMaxWidth().testTag("block.rich.code"),
) {
Column(Modifier.padding(12.dp)) {
if (language.isNotBlank()) {
if (LocalShowCodeLanguage.current && language.isNotBlank()) {
Text(language, color = MaterialTheme.colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.labelSmall, modifier = Modifier.padding(bottom = 6.dp))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ private const val RICH_FINGERPRINT =
"ios/swiftui|standard-children:0|children:|" +
"properties:10:asset-type:string:required:none,10:local-path:string:" +
"required:none,11:markup-json:string:required:none,12:is-completed:bool:" +
"required:none,18:youtube-target-url:string:required:none,5:title:string:" +
"required:none,18:show-code-language:bool:optional:none,18:youtube-target-url:string:required:none,5:title:string:" +
"required:none,8:block-id:string:required:none,8:is-asset:bool:required:none|" +
"events:10:drag-start[4:uuid:string:required],4:drop[4:uuid:string:required," +
"9:placement:string:required],4:edit[4:uuid:string:required],9:open-node[" +
Expand Down Expand Up @@ -161,6 +161,7 @@ fun logseqExtensionRegistry(
LuiExtensionProperty("local-path", LuiExtensionValueKind.STRING),
LuiExtensionProperty("markup-json", LuiExtensionValueKind.STRING),
LuiExtensionProperty("is-completed", LuiExtensionValueKind.BOOLEAN),
LuiExtensionProperty("show-code-language", LuiExtensionValueKind.BOOLEAN, required = false),
LuiExtensionProperty("youtube-target-url", LuiExtensionValueKind.STRING),
LuiExtensionProperty("title", LuiExtensionValueKind.STRING),
LuiExtensionProperty("block-id", LuiExtensionValueKind.STRING),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
Expand Down Expand Up @@ -223,14 +224,16 @@ internal fun OutlinerBlockContent(
},
)
} else {
ProjectedMarkup(
youtubeStarts = youtubeStarts,
encoded = context.string("markup-json") ?: "",
fallback = title,
completedDecoration = decoration,
onOpenNode = { context.emit("open-node", "uuid" to it) },
onTapFallback = { context.emit("edit", "uuid" to uuid) },
)
CompositionLocalProvider(LocalShowCodeLanguage provides (context.bool("show-code-language") ?: true)) {
ProjectedMarkup(
youtubeStarts = youtubeStarts,
encoded = context.string("markup-json") ?: "",
fallback = title,
completedDecoration = decoration,
onOpenNode = { context.emit("open-node", "uuid" to it) },
onTapFallback = { context.emit("edit", "uuid" to uuid) },
)
}
}
}
if (dragHover) {
Expand Down
7 changes: 6 additions & 1 deletion apple/Sources/Logseq/LGRichBlockExtension.swift
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ struct OutlinerRowDropDelegate: DropDelegate {
@MainActor
private enum LGRichBlockExtension {
static let identifier = "outliner-block-content"
static let fingerprint = "lui-extension-v1|22:outliner-block-content|profiles:android/kotlin,ios/swiftui|standard-children:0|children:|properties:10:asset-type:string:required:none,10:local-path:string:required:none,11:markup-json:string:required:none,12:is-completed:bool:required:none,18:youtube-target-url:string:required:none,5:title:string:required:none,8:block-id:string:required:none,8:is-asset:bool:required:none|events:10:drag-start[4:uuid:string:required],4:drop[4:uuid:string:required,9:placement:string:required],4:edit[4:uuid:string:required],9:open-node[4:uuid:string:required]"
static let fingerprint = "lui-extension-v1|22:outliner-block-content|profiles:android/kotlin,ios/swiftui|standard-children:0|children:|properties:10:asset-type:string:required:none,10:local-path:string:required:none,11:markup-json:string:required:none,12:is-completed:bool:required:none,18:show-code-language:bool:optional:none,18:youtube-target-url:string:required:none,5:title:string:required:none,8:block-id:string:required:none,8:is-asset:bool:required:none|events:10:drag-start[4:uuid:string:required],4:drop[4:uuid:string:required,9:placement:string:required],4:edit[4:uuid:string:required],9:open-node[4:uuid:string:required]"

static func register(
in registry: LUIAppleExtensionRegistry,
Expand All @@ -118,6 +118,7 @@ private enum LGRichBlockExtension {
.init(name: "youtube-target-url", kind: .string, isRequired: true),
.init(name: "is-asset", kind: .bool, isRequired: true),
.init(name: "is-completed", kind: .bool, isRequired: true),
.init(name: "show-code-language", kind: .bool, isRequired: false),
.init(name: "asset-type", kind: .string, isRequired: true),
.init(name: "local-path", kind: .string, isRequired: true),
],
Expand Down Expand Up @@ -199,6 +200,10 @@ private struct LGRichBlock: View {
}
)
.padding(.vertical, titleVerticalInset)
.environment(\.lgShowCodeLanguage, {
if case let .bool(value) = context.property("show-code-language") { return value }
return true
}())
}
}
.font(propertyValue ? .callout : .body)
Expand Down
16 changes: 14 additions & 2 deletions apple/Sources/Logseq/OutlinerRichBlockContent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -366,7 +366,19 @@ private struct InteractiveCloze: View {
}
}

private struct ShowCodeLanguageKey: EnvironmentKey {
static let defaultValue = true
}

extension EnvironmentValues {
var lgShowCodeLanguage: Bool {
get { self[ShowCodeLanguageKey.self] }
set { self[ShowCodeLanguageKey.self] = newValue }
}
}

private struct SyntaxHighlightedCodeBlock: View {
@Environment(\.lgShowCodeLanguage) private var showLanguage
let code: String
let language: String?

Expand All @@ -377,7 +389,7 @@ private struct SyntaxHighlightedCodeBlock: View {

var body: some View {
VStack(alignment: .leading, spacing: 6) {
if let language, !language.isEmpty {
if showLanguage, let language, !language.isEmpty {
Text(verbatim: language)
.font(.caption2.monospaced())
.foregroundStyle(.secondary)
Expand All @@ -391,7 +403,7 @@ private struct SyntaxHighlightedCodeBlock: View {
}
}
.padding(10)
.background(Color.secondary.opacity(0.1))
.background(showLanguage ? Color.secondary.opacity(0.1) : Color.clear)
.clipShape(RoundedRectangle(cornerRadius: 8))
}
}
Expand Down
19 changes: 15 additions & 4 deletions apple/Sources/Logseq/OutlinerRowExtension.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ struct OutlinerRowPayload: Decodable, Equatable {
let youtubeURL: String
let completed: Bool
let selected: Bool
let editing: Bool?
let hasCustomContent: Bool?
let opensAsPage: Bool
let nodeIcon: NodeIconPayload?
let listNumber: Int?
Expand Down Expand Up @@ -123,13 +125,18 @@ struct OutlinerRowPayload: Decodable, Equatable {
@Environment(\.colorScheme) private var colorScheme
@Environment(\.openURL) private var openURL

private var hasCustomContent: Bool {
row.hasCustomContent ?? !context.childIDs.isEmpty
}

@ViewBuilder var body: some View {
#if os(iOS)
if context.childIDs.isEmpty {
if !(row.editing ?? !context.childIDs.isEmpty) {
content
.overlay { OutlinerDropIndicator(placement: dropGeometry.placement, depth: row.depth) }
.onGeometryChange(for: CGFloat.self) { $0.size.height } action: { dropGeometry.height = $0 }
.onLongPressGesture { emit("long-press") }
.contentShape(Rectangle())
.highPriorityGesture(LongPressGesture().onEnded { _ in emit("long-press") })
.onDrag { emit("drag-start"); return NSItemProvider(object: row.uuid as NSString) }
.onDrop(of: [UTType.plainText], delegate: OutlinerRowDropDelegate(geometry: dropGeometry, depth: row.depth) {
emit("drop", extra: ["placement": .string($0.eventValue)])
Expand All @@ -144,7 +151,7 @@ struct OutlinerRowPayload: Decodable, Equatable {
@ViewBuilder private var content: some View {
Group {
#if os(iOS)
if context.childIDs.isEmpty && OutlinerTextRow.supports(row) {
if !hasCustomContent && OutlinerTextRow.supports(row) {
OutlinerTextRow(row: row,
emit: { name, uuid, values in emit(name, uuid: uuid, extra: values) },
onOpenLink: { url in
Expand Down Expand Up @@ -185,6 +192,9 @@ struct OutlinerRowPayload: Decodable, Equatable {
.accessibilityIdentifier("button.outliner.zoom." + row.uuid + "." + row.title)
}
}
.simultaneousGesture(LongPressGesture().onEnded { _ in
if row.editing == true { emit("long-press") }
})
.padding(.trailing, row.status != nil
? OutlinerLayoutMetrics.statusLeading - OutlinerLayoutMetrics.bulletHitSize
: (row.priority != nil ? -4 : OutlinerLayoutMetrics.bulletContentSpacing))
Expand Down Expand Up @@ -232,8 +242,9 @@ struct OutlinerRowPayload: Decodable, Equatable {
.accessibilityIdentifier("button.block-task-priority")
}
VStack(alignment: .leading, spacing: 2) {
if let editor = context.childIDs.first {
if hasCustomContent, let editor = context.childIDs.first {
context.content(for: editor)
.font(row.markup.contains { $0.type == .codeBlock } ? .caption : .body)
} else if row.isAsset {
LGAssetPreview(uuid: row.uuid, title: row.title, assetType: row.assetType, localPath: row.localPath)
} else {
Expand Down
2 changes: 1 addition & 1 deletion apple/Tests/LogseqTests/LGRendererTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ struct LGRendererTests {
try renderer.apply(patchJSON: """
{"generation":1,"ops":[
{"op":"create-node","id":1,"kind":"root"},
{"op":"create-extension","id":2,"identifier":"outliner-block-content","fingerprint":"lui-extension-v1|22:outliner-block-content|profiles:android/kotlin,ios/swiftui|standard-children:0|children:|properties:10:asset-type:string:required:none,10:local-path:string:required:none,11:markup-json:string:required:none,12:is-completed:bool:required:none,18:youtube-target-url:string:required:none,5:title:string:required:none,8:block-id:string:required:none,8:is-asset:bool:required:none|events:10:drag-start[4:uuid:string:required],4:drop[4:uuid:string:required,9:placement:string:required],4:edit[4:uuid:string:required],9:open-node[4:uuid:string:required]"},
{"op":"create-extension","id":2,"identifier":"outliner-block-content","fingerprint":"lui-extension-v1|22:outliner-block-content|profiles:android/kotlin,ios/swiftui|standard-children:0|children:|properties:10:asset-type:string:required:none,10:local-path:string:required:none,11:markup-json:string:required:none,12:is-completed:bool:required:none,18:show-code-language:bool:optional:none,18:youtube-target-url:string:required:none,5:title:string:required:none,8:block-id:string:required:none,8:is-asset:bool:required:none|events:10:drag-start[4:uuid:string:required],4:drop[4:uuid:string:required,9:placement:string:required],4:edit[4:uuid:string:required],9:open-node[4:uuid:string:required]"},
{"op":"set-extension-prop","id":2,"property":"block-id","value":"block-a"},
{"op":"set-extension-prop","id":2,"property":"title","value":"Project"},
{"op":"set-extension-prop","id":2,"property":"markup-json","value":"[]"},
Expand Down
Loading
Loading