Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cc27a860e5
|
||
|
|
d2e213b516
|
||
|
|
226d6fb748
|
||
|
|
44a2aff050
|
||
|
|
d44e4dd4ec
|
@@ -1,3 +1,6 @@
|
||||
[submodule "docs/reference/outline-openapi"]
|
||||
path = docs/reference/outline-openapi
|
||||
url = https://github.com/outline/openapi.git
|
||||
[submodule "Vendor/swift-markdown-engine"]
|
||||
path = Vendor/swift-markdown-engine
|
||||
url = https://github.com/nodes-app/swift-markdown-engine.git
|
||||
|
||||
@@ -7,10 +7,10 @@
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
FA7596B230366A1D0000167E /* MarkdownEngine in Frameworks */ = {isa = PBXBuildFile; productRef = FA7596B130366A1D0000167E /* MarkdownEngine */; };
|
||||
FA7596B430366A1D0000167E /* MarkdownEngineCodeBlocks in Frameworks */ = {isa = PBXBuildFile; productRef = FA7596B330366A1D0000167E /* MarkdownEngineCodeBlocks */; };
|
||||
FA7596B630366A1D0000167E /* MarkdownEngineLatex in Frameworks */ = {isa = PBXBuildFile; productRef = FA7596B530366A1D0000167E /* MarkdownEngineLatex */; };
|
||||
FAF99C44302CF1BD00C9949F /* OutlineKit in Frameworks */ = {isa = PBXBuildFile; productRef = FAF99C43302CF1BD00C9949F /* OutlineKit */; };
|
||||
FAF99CAF302D120500C9949F /* MarkdownEngine in Frameworks */ = {isa = PBXBuildFile; productRef = FAF99CAE302D120500C9949F /* MarkdownEngine */; };
|
||||
FAF99CB1302D120500C9949F /* MarkdownEngineCodeBlocks in Frameworks */ = {isa = PBXBuildFile; productRef = FAF99CB0302D120500C9949F /* MarkdownEngineCodeBlocks */; };
|
||||
FAF99CB3302D120500C9949F /* MarkdownEngineLatex in Frameworks */ = {isa = PBXBuildFile; productRef = FAF99CB2302D120500C9949F /* MarkdownEngineLatex */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
@@ -59,9 +59,9 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
files = (
|
||||
FAF99C44302CF1BD00C9949F /* OutlineKit in Frameworks */,
|
||||
FAF99CB1302D120500C9949F /* MarkdownEngineCodeBlocks in Frameworks */,
|
||||
FAF99CB3302D120500C9949F /* MarkdownEngineLatex in Frameworks */,
|
||||
FAF99CAF302D120500C9949F /* MarkdownEngine in Frameworks */,
|
||||
FA7596B430366A1D0000167E /* MarkdownEngineCodeBlocks in Frameworks */,
|
||||
FA7596B630366A1D0000167E /* MarkdownEngineLatex in Frameworks */,
|
||||
FA7596B230366A1D0000167E /* MarkdownEngine in Frameworks */,
|
||||
);
|
||||
};
|
||||
FAF99C24302CE96200C9949F /* Frameworks */ = {
|
||||
@@ -116,9 +116,9 @@
|
||||
name = Outpost;
|
||||
packageProductDependencies = (
|
||||
FAF99C43302CF1BD00C9949F /* OutlineKit */,
|
||||
FAF99CAE302D120500C9949F /* MarkdownEngine */,
|
||||
FAF99CB0302D120500C9949F /* MarkdownEngineCodeBlocks */,
|
||||
FAF99CB2302D120500C9949F /* MarkdownEngineLatex */,
|
||||
FA7596B130366A1D0000167E /* MarkdownEngine */,
|
||||
FA7596B330366A1D0000167E /* MarkdownEngineCodeBlocks */,
|
||||
FA7596B530366A1D0000167E /* MarkdownEngineLatex */,
|
||||
);
|
||||
productName = Outpost;
|
||||
productReference = FAF99C18302CE96100C9949F /* Outpost.app */;
|
||||
@@ -200,7 +200,7 @@
|
||||
minimizedProjectReferenceProxies = 1;
|
||||
packageReferences = (
|
||||
FAF99C42302CF1BD00C9949F /* XCLocalSwiftPackageReference "OutlineKit" */,
|
||||
FAF99CAD302D120500C9949F /* XCRemoteSwiftPackageReference "swift-markdown-engine" */,
|
||||
FA7596B030366A1D0000167E /* XCLocalSwiftPackageReference "Vendor/swift-markdown-engine" */,
|
||||
);
|
||||
preferredProjectObjectVersion = 77;
|
||||
productRefGroup = FAF99C19302CE96100C9949F /* Products */;
|
||||
@@ -622,43 +622,33 @@
|
||||
/* End XCConfigurationList section */
|
||||
|
||||
/* Begin XCLocalSwiftPackageReference section */
|
||||
FA7596B030366A1D0000167E /* XCLocalSwiftPackageReference "Vendor/swift-markdown-engine" */ = {
|
||||
isa = XCLocalSwiftPackageReference;
|
||||
relativePath = "Vendor/swift-markdown-engine";
|
||||
};
|
||||
FAF99C42302CF1BD00C9949F /* XCLocalSwiftPackageReference "OutlineKit" */ = {
|
||||
isa = XCLocalSwiftPackageReference;
|
||||
relativePath = OutlineKit;
|
||||
};
|
||||
/* End XCLocalSwiftPackageReference section */
|
||||
|
||||
/* Begin XCRemoteSwiftPackageReference section */
|
||||
FAF99CAD302D120500C9949F /* XCRemoteSwiftPackageReference "swift-markdown-engine" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/nodes-app/swift-markdown-engine";
|
||||
requirement = {
|
||||
kind = upToNextMajorVersion;
|
||||
minimumVersion = 0.12.0;
|
||||
};
|
||||
};
|
||||
/* End XCRemoteSwiftPackageReference section */
|
||||
|
||||
/* Begin XCSwiftPackageProductDependency section */
|
||||
FA7596B130366A1D0000167E /* MarkdownEngine */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
productName = MarkdownEngine;
|
||||
};
|
||||
FA7596B330366A1D0000167E /* MarkdownEngineCodeBlocks */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
productName = MarkdownEngineCodeBlocks;
|
||||
};
|
||||
FA7596B530366A1D0000167E /* MarkdownEngineLatex */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
productName = MarkdownEngineLatex;
|
||||
};
|
||||
FAF99C43302CF1BD00C9949F /* OutlineKit */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
productName = OutlineKit;
|
||||
};
|
||||
FAF99CAE302D120500C9949F /* MarkdownEngine */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = FAF99CAD302D120500C9949F /* XCRemoteSwiftPackageReference "swift-markdown-engine" */;
|
||||
productName = MarkdownEngine;
|
||||
};
|
||||
FAF99CB0302D120500C9949F /* MarkdownEngineCodeBlocks */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = FAF99CAD302D120500C9949F /* XCRemoteSwiftPackageReference "swift-markdown-engine" */;
|
||||
productName = MarkdownEngineCodeBlocks;
|
||||
};
|
||||
FAF99CB2302D120500C9949F /* MarkdownEngineLatex */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = FAF99CAD302D120500C9949F /* XCRemoteSwiftPackageReference "swift-markdown-engine" */;
|
||||
productName = MarkdownEngineLatex;
|
||||
};
|
||||
/* End XCSwiftPackageProductDependency section */
|
||||
};
|
||||
rootObject = FAF99C10302CE96100C9949F /* Project object */;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"originHash" : "f233fa96f0c6bdcdbf87f726af38f25704f6d46a156a27dfac47541baa63bf97",
|
||||
"originHash" : "4127e8224149bef00a33500e8db49748a735e1c07626f2255faea52554afef5e",
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "highlighterswift",
|
||||
@@ -10,15 +10,6 @@
|
||||
"version" : "3.1.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-markdown-engine",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/nodes-app/swift-markdown-engine",
|
||||
"state" : {
|
||||
"revision" : "e5f7607fc4021181056ef7a09dbb7573dc0237d9",
|
||||
"version" : "0.12.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swiftmath",
|
||||
"kind" : "remoteSourceControl",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#if os(macOS)
|
||||
import SwiftUI
|
||||
import MarkdownEngine
|
||||
import MarkdownEngineCodeBlocks
|
||||
import OutlineKit
|
||||
|
||||
/// Read-only for now — document/overview editing isn't wired up yet. Uses
|
||||
@@ -18,7 +19,10 @@ struct CollectionOverviewContent: View {
|
||||
ScrollView {
|
||||
NativeTextViewWrapper(
|
||||
text: $markdown,
|
||||
configuration: .init(heightBehavior: .fitsContent),
|
||||
configuration: .init(
|
||||
services: .init(syntaxHighlighter: CodeSyntaxHighlighting.shared),
|
||||
heightBehavior: .fitsContent
|
||||
),
|
||||
isEditable: false
|
||||
)
|
||||
.padding()
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#if os(macOS)
|
||||
import SwiftUI
|
||||
import MarkdownEngine
|
||||
import MarkdownEngineCodeBlocks
|
||||
import OutlineKit
|
||||
|
||||
/// Distraction-free reading view — no toolbar/sidebar chrome, larger type.
|
||||
@@ -44,7 +45,10 @@ struct DocumentPresentSheet: View {
|
||||
.padding(.bottom, 8)
|
||||
NativeTextViewWrapper(
|
||||
text: $text,
|
||||
configuration: .init(heightBehavior: .fitsContent),
|
||||
configuration: .init(
|
||||
services: .init(syntaxHighlighter: CodeSyntaxHighlighting.shared),
|
||||
heightBehavior: .fitsContent
|
||||
),
|
||||
isEditable: false
|
||||
)
|
||||
.font(.system(size: 18))
|
||||
|
||||
@@ -3,6 +3,7 @@ import AppKit
|
||||
import SwiftUI
|
||||
import UniformTypeIdentifiers
|
||||
import MarkdownEngine
|
||||
import MarkdownEngineCodeBlocks
|
||||
import OutlineKit
|
||||
|
||||
/// `NSSavePanel`/`NSPrintOperation`/`NSPasteboard` in the action functions
|
||||
@@ -17,6 +18,21 @@ struct DocumentReaderView: View {
|
||||
/// Outline — see `SettingsView.editorDetail`.
|
||||
@AppStorage("outpost.splitViewEnabled") private var isSplitViewEnabled = false
|
||||
|
||||
/// Outline's own "Show line numbers" preference (synced, read via
|
||||
/// `session.userPreferences`, not `@AppStorage` — this one's the
|
||||
/// server's, not a local-only Outpost setting). No `@Environment`-in-`init`
|
||||
/// problem here since this is read directly in the view, not the
|
||||
/// view model.
|
||||
private var showCodeBlockLineNumbers: Bool {
|
||||
session.userPreferences?.codeBlockLineNumbers ?? false
|
||||
}
|
||||
/// Widened left indent reserved for the number gutter when line numbers
|
||||
/// are on (default is 12pt, just enough margin, no room for digits).
|
||||
private static let lineNumberGutterWidth: CGFloat = 32
|
||||
private var editorCodeBlockStyle: CodeBlockStyle {
|
||||
showCodeBlockLineNumbers ? .init(horizontalIndent: Self.lineNumberGutterWidth) : .default
|
||||
}
|
||||
|
||||
@State private var viewModel: DocumentReaderViewModel
|
||||
let apiClient: OutlineAPIClient
|
||||
let document: OutlineDocument
|
||||
@@ -44,6 +60,14 @@ struct DocumentReaderView: View {
|
||||
@State private var isShowingShareSheet = false
|
||||
@State private var isShowingNewDocumentSheet = false
|
||||
@State private var actionErrorMessage: String?
|
||||
/// Populated live by `NativeTextViewWrapper`'s `onCodeBlockSelectionChange` —
|
||||
/// one array per instance (main pane, split-view preview pane), since
|
||||
/// each lays the same text out at a different width and gets different
|
||||
/// rects. Only non-empty when `showCodeBlockLineNumbers` is on (see its
|
||||
/// doc comment for why the gutter needs `codeBlock.horizontalIndent`
|
||||
/// widened, which is gated on the same flag).
|
||||
@State private var readerCodeBlocks: [CodeBlockSelection] = []
|
||||
@State private var previewCodeBlocks: [CodeBlockSelection] = []
|
||||
|
||||
init(
|
||||
apiClient: OutlineAPIClient,
|
||||
@@ -307,12 +331,24 @@ struct DocumentReaderView: View {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ZStack(alignment: .topLeading) {
|
||||
NativeTextViewWrapper(
|
||||
text: $viewModel.text,
|
||||
configuration: .init(heightBehavior: .fitsContent),
|
||||
configuration: .init(
|
||||
services: .init(syntaxHighlighter: CodeSyntaxHighlighting.shared),
|
||||
codeBlock: editorCodeBlockStyle,
|
||||
heightBehavior: .fitsContent
|
||||
),
|
||||
documentId: viewModel.documentId,
|
||||
isEditable: viewModel.isEffectivelyEditable
|
||||
isEditable: viewModel.isEffectivelyEditable,
|
||||
onCodeBlockSelectionChange: { readerCodeBlocks = $0 }
|
||||
)
|
||||
if showCodeBlockLineNumbers {
|
||||
ForEach(readerCodeBlocks) { selection in
|
||||
CodeBlockLineNumberGutter(selection: selection, gutterWidth: Self.lineNumberGutterWidth)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
@@ -360,12 +396,24 @@ struct DocumentReaderView: View {
|
||||
.frame(minWidth: 300, maxWidth: .infinity, maxHeight: .infinity)
|
||||
|
||||
ScrollView {
|
||||
ZStack(alignment: .topLeading) {
|
||||
NativeTextViewWrapper(
|
||||
text: $viewModel.text,
|
||||
configuration: .init(heightBehavior: .fitsContent),
|
||||
configuration: .init(
|
||||
services: .init(syntaxHighlighter: CodeSyntaxHighlighting.shared),
|
||||
codeBlock: editorCodeBlockStyle,
|
||||
heightBehavior: .fitsContent
|
||||
),
|
||||
documentId: viewModel.documentId,
|
||||
isEditable: false
|
||||
isEditable: false,
|
||||
onCodeBlockSelectionChange: { previewCodeBlocks = $0 }
|
||||
)
|
||||
if showCodeBlockLineNumbers {
|
||||
ForEach(previewCodeBlocks) { selection in
|
||||
CodeBlockLineNumberGutter(selection: selection, gutterWidth: Self.lineNumberGutterWidth)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(8)
|
||||
.frame(maxWidth: .infinity, alignment: .topLeading)
|
||||
}
|
||||
@@ -610,4 +658,58 @@ struct DocumentReaderView: View {
|
||||
operation.run()
|
||||
}
|
||||
}
|
||||
|
||||
/// One code block's number gutter, positioned absolutely over a
|
||||
/// `NativeTextViewWrapper` via `CodeBlockSelection.rect` — same overlay
|
||||
/// pattern MarkdownEngine's own `CodeBlockButton` uses.
|
||||
///
|
||||
/// `selection.rect` spans the WHOLE fenced block (open fence line + content
|
||||
/// + close fence line), matching what the engine actually lays out — the
|
||||
/// fence lines render with invisible (`.clear`) text once the caret leaves
|
||||
/// the block, but they don't collapse to zero height, so the block is
|
||||
/// always exactly `content line count + 2` rows tall. `selection.code` is
|
||||
/// content only, so the row height and number positions below both account
|
||||
/// for that phantom top/bottom row explicitly instead of dividing by the
|
||||
/// content line count alone (which would drift the numbers upward, more so
|
||||
/// per line, the taller the block).
|
||||
///
|
||||
/// Known limitation, accepted rather than fixable app-side: a content line
|
||||
/// that soft-wraps onto a second visual row (MarkdownEngine always
|
||||
/// char-wraps code blocks, no way to opt out without forking the package)
|
||||
/// throws this off — every row below it reads one line low. Documented in
|
||||
/// TODO.local.md alongside the same package's other gaps.
|
||||
private struct CodeBlockLineNumberGutter: View {
|
||||
let selection: CodeBlockSelection
|
||||
let gutterWidth: CGFloat
|
||||
|
||||
/// `selection.code` (`token.contentRange`) always ends with exactly one
|
||||
/// trailing `\n` per content line — the range runs right up to the
|
||||
/// start of the closing fence's own line, so the newline that ends the
|
||||
/// last content line is included, but there's never an unterminated
|
||||
/// final line to add one more for. Counting `\n` characters directly
|
||||
/// (not `.components(separatedBy:).count`, which is one too many
|
||||
/// whenever the string ends in the separator) is what makes a
|
||||
/// single-line block read "1", not "2".
|
||||
private var contentLineCount: Int {
|
||||
max(1, selection.code.reduce(into: 0) { count, char in if char == "\n" { count += 1 } })
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
let totalRows = CGFloat(contentLineCount + 2)
|
||||
let rowHeight = selection.rect.height / totalRows
|
||||
ForEach(0..<contentLineCount, id: \.self) { line in
|
||||
Text("\(line + 1)")
|
||||
.font(.system(size: 10, design: .monospaced))
|
||||
.foregroundStyle(.secondary)
|
||||
.frame(width: gutterWidth - 6, alignment: .trailing)
|
||||
.position(
|
||||
x: selection.rect.minX + (gutterWidth - 6) / 2,
|
||||
// +1.5 rows: skip the invisible open-fence row, then
|
||||
// center within this content row.
|
||||
y: selection.rect.minY + rowHeight * (CGFloat(line) + 1.5)
|
||||
)
|
||||
}
|
||||
.allowsHitTesting(false)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
#if os(macOS)
|
||||
import MarkdownEngineCodeBlocks
|
||||
|
||||
/// One `HighlighterSwiftBridge` for the whole app. It owns a JavaScriptCore
|
||||
/// context (expensive to spin up) plus its own highlight cache, so every
|
||||
/// `NativeTextViewWrapper` should share this instance rather than each
|
||||
/// constructing its own.
|
||||
enum CodeSyntaxHighlighting {
|
||||
static let shared = HighlighterSwiftBridge()
|
||||
}
|
||||
#endif
|
||||
+1
Submodule Vendor/swift-markdown-engine added at 05c1720f44
Reference in New Issue
Block a user