Skip to content

Commit dbb79c2

Browse files
committed
Combine statistics for selection and full doc
1 parent 91ba175 commit dbb79c2

File tree

10 files changed

+177
-68
lines changed

10 files changed

+177
-68
lines changed

CoreEditor/src/bridge/web/core.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { WebModule } from '../webModule';
22
import {
3-
ReadableContent,
3+
ReadableContentPair,
44
ReplaceGranularity,
55
resetEditor,
66
clearEditor,
@@ -22,7 +22,7 @@ export interface WebModuleCore extends WebModule {
2222
resetEditor({ text }: { text: string }): void;
2323
clearEditor(): void;
2424
getEditorText(): string;
25-
getReadableContent(): ReadableContent;
25+
getReadableContent(): ReadableContentPair;
2626
insertText({ text, from, to }: { text: string; from: CodeGen_Int; to: CodeGen_Int }): void;
2727
replaceText({ text, granularity }: { text: string; granularity: ReplaceGranularity }): void;
2828
handleFocusLost(): void;
@@ -43,7 +43,7 @@ export class WebModuleCoreImpl implements WebModuleCore {
4343
return getEditorText();
4444
}
4545

46-
getReadableContent(): ReadableContent {
46+
getReadableContent(): ReadableContentPair {
4747
return getReadableContent();
4848
}
4949

CoreEditor/src/core.ts

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,15 @@ import { markContentClean } from './modules/history';
1919
import { TextEditor } from './api/editor';
2020
import { editorReadyListeners } from './api/methods';
2121

22-
export interface ReadableContent {
23-
selectionBased: boolean;
22+
type ReadableContent = {
2423
sourceText: string;
2524
trimmedText: string;
2625
commentCount: CodeGen_Int;
26+
};
27+
28+
export interface ReadableContentPair {
29+
fullText: ReadableContent;
30+
selection?: ReadableContent;
2731
}
2832

2933
export enum ReplaceGranularity {
@@ -164,22 +168,26 @@ export function getEditorText() {
164168
return lines.join(state.lineBreak);
165169
}
166170

167-
export function getReadableContent(): ReadableContent {
168-
// Extract the source to show the statistics view,
169-
// use the selected text if the selection is not empty.
170-
const selectedText = selectedMainText();
171-
const selectionBased = selectedText.length > 0;
172-
const sourceText = selectionBased ? selectedText : getEditorText();
173-
174-
// Remove front matter and extract comments
175-
const actualText = removeFrontMatter(sourceText);
176-
const { trimmedText, commentCount } = extractComments(actualText);
171+
export function getReadableContent(): ReadableContentPair {
172+
const getContent = (sourceText: string): ReadableContent => {
173+
// Remove front matter and extract comments
174+
const actualText = removeFrontMatter(sourceText);
175+
const { trimmedText, commentCount } = extractComments(actualText);
176+
177+
return {
178+
sourceText,
179+
trimmedText,
180+
commentCount: commentCount as CodeGen_Int,
181+
};
182+
};
177183

178184
return {
179-
selectionBased,
180-
sourceText,
181-
trimmedText,
182-
commentCount: commentCount as CodeGen_Int,
185+
fullText: getContent(getEditorText()),
186+
selection: (() => {
187+
// Get readable content for selection if the selection is not empty.
188+
const selectedText = selectedMainText();
189+
return selectedText.length > 0 ? getContent(selectedText) : undefined;
190+
})(),
183191
};
184192
}
185193

MarkEditKit/Sources/Bridge/Web/Generated/WebBridgeCore.swift

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ public final class WebBridgeCore {
4444
}
4545
}
4646

47-
public func getReadableContent() async throws -> ReadableContent {
47+
public func getReadableContent() async throws -> ReadableContentPair {
4848
return try await withCheckedThrowingContinuation { continuation in
4949
webView?.invoke(path: "webModules.core.getReadableContent") { result in
5050
Task { @MainActor in
@@ -115,14 +115,22 @@ public final class WebBridgeCore {
115115
}
116116
}
117117

118+
public struct ReadableContentPair: Codable {
119+
public var fullText: ReadableContent
120+
public var selection: ReadableContent?
121+
122+
public init(fullText: ReadableContent, selection: ReadableContent?) {
123+
self.fullText = fullText
124+
self.selection = selection
125+
}
126+
}
127+
118128
public struct ReadableContent: Codable {
119-
public var selectionBased: Bool
120129
public var sourceText: String
121130
public var trimmedText: String
122131
public var commentCount: Int
123132

124-
public init(selectionBased: Bool, sourceText: String, trimmedText: String, commentCount: Int) {
125-
self.selectionBased = selectionBased
133+
public init(sourceText: String, trimmedText: String, commentCount: Int) {
126134
self.sourceText = sourceText
127135
self.trimmedText = trimmedText
128136
self.commentCount = commentCount

MarkEditMac/Modules/Package.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ let package = Package(
9191
),
9292
.target(
9393
name: "FileVersion",
94-
dependencies: ["MarkEditKit", "AppKitControls"],
94+
dependencies: ["AppKitControls", "MarkEditKit"],
9595
path: "Sources/FileVersion",
9696
swiftSettings: [
9797
.enableExperimentalFeature("StrictConcurrency")
@@ -113,7 +113,7 @@ let package = Package(
113113
),
114114
.target(
115115
name: "Previewer",
116-
dependencies: ["MarkEditKit", "AppKitExtensions"],
116+
dependencies: ["AppKitExtensions", "MarkEditKit"],
117117
path: "Sources/Previewer",
118118
resources: [
119119
.process("Resources"),
@@ -138,7 +138,7 @@ let package = Package(
138138
),
139139
.target(
140140
name: "Statistics",
141-
dependencies: ["AppKitExtensions"],
141+
dependencies: ["AppKitExtensions", "MarkEditKit"],
142142
path: "Sources/Statistics",
143143
swiftSettings: [
144144
.enableExperimentalFeature("StrictConcurrency")

MarkEditMac/Modules/Sources/Statistics/StatisticsController.swift

Lines changed: 29 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
import AppKit
88
import SwiftUI
9+
import MarkEditKit
910

1011
/**
1112
UI to show statistics of text.
@@ -16,23 +17,17 @@ public final class StatisticsController: NSViewController {
1617
static let contentHeight: Double = 288
1718
}
1819

19-
private let sourceText: String
20-
private let trimmedText: String
21-
private let commentCount: Int
20+
private let content: ReadableContentPair
2221
private let fileURL: URL?
2322
private let localizable: StatisticsLocalizable
2423
private var contentView: NSView?
2524

2625
public init(
27-
sourceText: String,
28-
trimmedText: String,
29-
commentCount: Int,
26+
content: ReadableContentPair,
3027
fileURL: URL?,
3128
localizable: StatisticsLocalizable
3229
) {
33-
self.sourceText = sourceText
34-
self.trimmedText = trimmedText
35-
self.commentCount = commentCount
30+
self.content = content
3631
self.fileURL = fileURL
3732
self.localizable = localizable
3833
super.init(nibName: nil, bundle: nil)
@@ -68,21 +63,19 @@ public final class StatisticsController: NSViewController {
6863
spinner.heightAnchor.constraint(equalToConstant: 24),
6964
])
7065

71-
DispatchQueue.global(qos: .userInitiated).async {
72-
// Natural language processing is time-consuming for large documents
73-
let tokenizedResult = Tokenizer.tokenize(
74-
sourceText: self.sourceText,
75-
trimmedText: self.trimmedText,
76-
commentCount: self.commentCount
77-
)
66+
// Natural language processing is time-consuming for large documents
67+
Task.detached(priority: .userInitiated) {
68+
let fullResult = self.content.fullText.tokenized
69+
let selectionResult = self.content.selection?.tokenized
7870

7971
// Remove the spinner and show the result view on main thread
8072
DispatchQueue.main.async {
8173
spinner.stopAnimation(nil)
8274
spinner.removeFromSuperview()
8375

8476
let contentView = NSHostingView(rootView: StatisticsView(
85-
tokenizedResult: tokenizedResult,
77+
fullResult: fullResult,
78+
selectionResult: selectionResult,
8679
fileURL: self.fileURL,
8780
localizable: self.localizable
8881
))
@@ -94,8 +87,27 @@ public final class StatisticsController: NSViewController {
9487
}
9588
}
9689

90+
override public func viewDidAppear() {
91+
super.viewDidAppear()
92+
view.window?.makeFirstResponder(self)
93+
}
94+
9795
override public func viewDidLayout() {
9896
super.viewDidLayout()
9997
contentView?.frame = view.bounds
10098
}
10199
}
100+
101+
extension ReadableContentPair: @unchecked @retroactive Sendable {}
102+
103+
// MARK: - Private
104+
105+
private extension ReadableContent {
106+
var tokenized: Tokenizer.Result {
107+
Tokenizer.tokenize(
108+
sourceText: sourceText,
109+
trimmedText: trimmedText,
110+
commentCount: commentCount
111+
)
112+
}
113+
}

MarkEditMac/Modules/Sources/Statistics/StatisticsLocalizable.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import Foundation
88

99
public struct StatisticsLocalizable {
1010
let mainTitle: String
11+
let selection: String
12+
let document: String
1113
let characters: String
1214
let words: String
1315
let sentences: String
@@ -18,6 +20,8 @@ public struct StatisticsLocalizable {
1820

1921
public init(
2022
mainTitle: String,
23+
selection: String,
24+
document: String,
2125
characters: String,
2226
words: String,
2327
sentences: String,
@@ -27,6 +31,8 @@ public struct StatisticsLocalizable {
2731
fileSize: String
2832
) {
2933
self.mainTitle = mainTitle
34+
self.selection = selection
35+
self.document = document
3036
self.characters = characters
3137
self.words = words
3238
self.sentences = sentences

MarkEditMac/Modules/Sources/Statistics/Views/StatisticsView.swift

Lines changed: 75 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,28 +5,67 @@
55
//
66

77
import AppKit
8+
import AppKitExtensions
89
import SwiftUI
910

1011
struct StatisticsView: View {
11-
private let tokenizedResult: Tokenizer.Result
12+
private let fullResult: Tokenizer.Result
13+
private let selectionResult: Tokenizer.Result?
1214
private let fileURL: URL?
1315
private let localizable: StatisticsLocalizable
1416

17+
@State private var viewingMode: ViewingMode = .selection
18+
@State private var localMonitor: Any?
19+
1520
init(
16-
tokenizedResult: Tokenizer.Result,
21+
fullResult: Tokenizer.Result,
22+
selectionResult: Tokenizer.Result?,
1723
fileURL: URL?,
1824
localizable: StatisticsLocalizable
1925
) {
20-
self.tokenizedResult = tokenizedResult
26+
self.fullResult = fullResult
27+
self.selectionResult = selectionResult
2128
self.fileURL = fileURL
2229
self.localizable = localizable
2330
}
2431

2532
var body: some View {
2633
VStack(spacing: 0) {
27-
Text(localizable.mainTitle)
28-
.fontWeight(.semibold)
29-
.frame(height: 36)
34+
VStack {
35+
if selectionResult != nil {
36+
Picker(localizable.mainTitle, selection: $viewingMode) {
37+
Text(localizable.selection).tag(ViewingMode.selection)
38+
Text(localizable.document).tag(ViewingMode.document)
39+
}
40+
.labelsHidden() // Hide the label while keeping the accessibility
41+
.pickerStyle(.segmented)
42+
.padding()
43+
.onAppear {
44+
localMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { event in
45+
switch event.keyCode {
46+
case .kVK_LeftArrow:
47+
viewingMode = .selection
48+
return nil
49+
case .kVK_RightArrow:
50+
viewingMode = .document
51+
return nil
52+
default:
53+
return event
54+
}
55+
}
56+
}
57+
.onDisappear {
58+
if let localMonitor {
59+
NSEvent.removeMonitor(localMonitor)
60+
self.localMonitor = nil
61+
}
62+
}
63+
} else {
64+
Text(localizable.mainTitle)
65+
.fontWeight(.semibold)
66+
}
67+
}
68+
.frame(height: 36)
3069

3170
Divider()
3271

@@ -72,7 +111,8 @@ struct StatisticsView: View {
72111
)
73112
}
74113

75-
if let fileSize = FileSize.readableSize(of: fileURL) {
114+
// The file size is shown only when we are viewing the full document
115+
if isViewingDocument, let fileSize = FileSize.readableSize(of: fileURL) {
76116
StatisticsCell(
77117
iconName: Icons.fileSize,
78118
titleText: localizable.fileSize,
@@ -91,12 +131,32 @@ struct StatisticsView: View {
91131

92132
// MARK: - Private
93133

94-
private enum Icons {
95-
static let characters = "textformat"
96-
static let words = "text.bubble"
97-
static let sentences = "textformat.abc.dottedunderline"
98-
static let paragraphs = "paragraphsign"
99-
static let comments = "eye.slash"
100-
static let readTime = "stopwatch"
101-
static let fileSize = "doc.text.below.ecg"
134+
private extension StatisticsView {
135+
enum ViewingMode: Int {
136+
case selection = 0
137+
case document = 1
138+
}
139+
140+
enum Icons {
141+
static let characters = "textformat"
142+
static let words = "text.bubble"
143+
static let sentences = "textformat.abc.dottedunderline"
144+
static let paragraphs = "paragraphsign"
145+
static let comments = "eye.slash"
146+
static let readTime = "stopwatch"
147+
static let fileSize = "doc.text.below.ecg"
148+
}
149+
150+
var isViewingDocument: Bool {
151+
// We are viewing full document if there's no selection, or we explicitly selected the document section
152+
selectionResult == nil || viewingMode == .document
153+
}
154+
155+
var tokenizedResult: Tokenizer.Result {
156+
if !isViewingDocument, let selectionResult {
157+
return selectionResult
158+
}
159+
160+
return fullResult
161+
}
102162
}

0 commit comments

Comments
 (0)