Skip to content

Commit 645456d

Browse files
Minimap (#302)
### Description Adds a minimap to the editor. Specific changes: - Created a new `MinimapView`. This view acts as the container view for the entire minimap contents including the separator, text bubbles, and scroll view. - New classes for the minimap: - `MinimapContentView` Displays the real contents of the minimap. The layout manager and selection manager place views and draw into this view. - `MinimapLineRenderer` uses the new layout overriding APIs to render small bubbles rather than text for the minimap. This delegates all bubble calculation and drawing to a custom view `MinimapLineFragmentView`. - `MinimapLineFragmentView` is a subclass of `LineFragmentView` that calculates and draws bubbles instead of text based on the foreground color of the line fragment it displays. - Added a new `showMinimap` parameter to `TextViewController` and `CodeEditSourceEditor` types. - Moved all content inset calculation to two methods. - `updateContentInsets` updates all relevant content insets for the scroll view, find panel, and minimap in one central place. This method is deterministic and builds on work @austincondiff and I discussed. - `updateTextInsets` updates the text view's insets. Example app: - Added minimap toggle - Moved toggles and settings to it's own view, it was getting large. ### Related Issues * #33 ### Checklist - [x] Render minimap. - [x] Make minimap scrollable. - [x] Add 'visible pane' to minimap. - [x] Add selections to minimap. - [x] I read and understood the [contributing guide](https://github.com/CodeEditApp/CodeEdit/blob/main/CONTRIBUTING.md) as well as the [code of conduct](https://github.com/CodeEditApp/CodeEdit/blob/main/CODE_OF_CONDUCT.md) - [x] The issues this PR addresses are related to each other - [x] My changes generate no new warnings - [x] My code builds and runs on my machine - [x] My changes are all related to the related issue above - [x] I documented my code ### Screenshots Demo resizing & toggling minimap with selections being drawn and responding to scrolling. https://github.com/user-attachments/assets/038187d0-df01-4d19-b51c-ce160a891607 Demo scrolling a long document https://github.com/user-attachments/assets/65a98f74-d201-443e-8f0a-3ee81361f54d Demo editing text, with selections, dragging, and syntax highlighting. Note that syntax highlights haven't been updated to highlight the *entire* minimap visible region. That will be done, but adding to this PR I think will complicate this PR too much. https://github.com/user-attachments/assets/6188d560-ffc5-4cef-b6b8-e3c711e8ca27
1 parent 15798b8 commit 645456d

33 files changed

+1153
-155
lines changed

Example/CodeEditSourceEditorExample/CodeEditSourceEditorExample.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
6C1365462B8A7F2D004A1D18 /* LanguagePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C1365452B8A7F2D004A1D18 /* LanguagePicker.swift */; };
2121
6C1365482B8A7FBF004A1D18 /* EditorTheme+Default.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C1365472B8A7FBF004A1D18 /* EditorTheme+Default.swift */; };
2222
6C13654D2B8A821E004A1D18 /* NSColor+Hex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C13654C2B8A821E004A1D18 /* NSColor+Hex.swift */; };
23+
6CF31D4E2DB6A252006A77FD /* StatusBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CF31D4D2DB6A252006A77FD /* StatusBar.swift */; };
2324
/* End PBXBuildFile section */
2425

2526
/* Begin PBXFileReference section */
@@ -37,6 +38,7 @@
3738
6C1365452B8A7F2D004A1D18 /* LanguagePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LanguagePicker.swift; sourceTree = "<group>"; };
3839
6C1365472B8A7FBF004A1D18 /* EditorTheme+Default.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EditorTheme+Default.swift"; sourceTree = "<group>"; };
3940
6C13654C2B8A821E004A1D18 /* NSColor+Hex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSColor+Hex.swift"; sourceTree = "<group>"; };
41+
6CF31D4D2DB6A252006A77FD /* StatusBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusBar.swift; sourceTree = "<group>"; };
4042
/* End PBXFileReference section */
4143

4244
/* Begin PBXFrameworksBuildPhase section */
@@ -115,6 +117,7 @@
115117
isa = PBXGroup;
116118
children = (
117119
6C1365312B8A7B94004A1D18 /* ContentView.swift */,
120+
6CF31D4D2DB6A252006A77FD /* StatusBar.swift */,
118121
6C1365452B8A7F2D004A1D18 /* LanguagePicker.swift */,
119122
1CB30C392DAA1C28008058A7 /* IndentPicker.swift */,
120123
);
@@ -209,6 +212,7 @@
209212
6C1365482B8A7FBF004A1D18 /* EditorTheme+Default.swift in Sources */,
210213
6C13654D2B8A821E004A1D18 /* NSColor+Hex.swift in Sources */,
211214
6C1365302B8A7B94004A1D18 /* CodeEditSourceEditorExampleDocument.swift in Sources */,
215+
6CF31D4E2DB6A252006A77FD /* StatusBar.swift in Sources */,
212216
6C13652E2B8A7B94004A1D18 /* CodeEditSourceEditorExampleApp.swift in Sources */,
213217
6C1365442B8A7EED004A1D18 /* String+Lines.swift in Sources */,
214218
1CB30C3A2DAA1C28008058A7 /* IndentPicker.swift in Sources */,

Example/CodeEditSourceEditorExample/CodeEditSourceEditorExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@
1414
"kind" : "remoteSourceControl",
1515
"location" : "https://github.com/CodeEditApp/CodeEditTextView.git",
1616
"state" : {
17-
"revision" : "66e10658b5a0199479b1534f9bef531df34d0a91",
18-
"version" : "0.9.1"
17+
"revision" : "a5912e60f6bac25cd1cdf8bb532e1125b21cf7f7",
18+
"version" : "0.10.1"
1919
}
2020
},
2121
{

Example/CodeEditSourceEditorExample/CodeEditSourceEditorExample/Views/ContentView.swift

Lines changed: 17 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ struct ContentView: View {
2626
@State private var isInLongParse = false
2727
@State private var settingsIsPresented: Bool = false
2828
@State private var treeSitterClient = TreeSitterClient()
29+
@AppStorage("showMinimap") private var showMinimap: Bool = true
2930
@State private var indentOption: IndentOption = .spaces(count: 4)
3031

3132
init(document: Binding<CodeEditSourceEditorExampleDocument>, fileURL: URL?) {
@@ -44,73 +45,28 @@ struct ContentView: View {
4445
indentOption: indentOption,
4546
lineHeight: 1.2,
4647
wrapLines: wrapLines,
48+
editorOverscroll: 0.3,
4749
cursorPositions: $cursorPositions,
4850
useThemeBackground: true,
4951
highlightProviders: [treeSitterClient],
5052
contentInsets: NSEdgeInsets(top: proxy.safeAreaInsets.top, left: 0, bottom: 28.0, right: 0),
51-
useSystemCursor: useSystemCursor
53+
additionalTextInsets: NSEdgeInsets(top: 1, left: 0, bottom: 1, right: 0),
54+
useSystemCursor: useSystemCursor,
55+
showMinimap: showMinimap
5256
)
5357
.overlay(alignment: .bottom) {
54-
HStack {
55-
Menu {
56-
Toggle("Wrap Lines", isOn: $wrapLines)
57-
if #available(macOS 14, *) {
58-
Toggle("Use System Cursor", isOn: $useSystemCursor)
59-
} else {
60-
Toggle("Use System Cursor", isOn: $useSystemCursor)
61-
.disabled(true)
62-
.help("macOS 14 required")
63-
}
64-
} label: {}
65-
.background {
66-
Image(systemName: "switch.2")
67-
.foregroundStyle(.secondary)
68-
.font(.system(size: 13.5, weight: .regular))
69-
}
70-
.menuStyle(.borderlessButton)
71-
.menuIndicator(.hidden)
72-
.frame(maxWidth: 18, alignment: .center)
73-
Spacer()
74-
Group {
75-
if isInLongParse {
76-
HStack(spacing: 5) {
77-
ProgressView()
78-
.controlSize(.small)
79-
Text("Parsing Document")
80-
}
81-
} else {
82-
Text(getLabel(cursorPositions))
83-
}
84-
}
85-
.foregroundStyle(.secondary)
86-
Divider()
87-
.frame(height: 12)
88-
LanguagePicker(language: $language)
89-
.buttonStyle(.borderless)
90-
IndentPicker(indentOption: $indentOption, enabled: document.text.isEmpty)
91-
.buttonStyle(.borderless)
92-
}
93-
.font(.subheadline)
94-
.fontWeight(.medium)
95-
.controlSize(.small)
96-
.padding(.horizontal, 8)
97-
.frame(height: 28)
98-
.background(.bar)
99-
.overlay(alignment: .top) {
100-
VStack {
101-
Divider()
102-
.overlay {
103-
if colorScheme == .dark {
104-
Color.black
105-
}
106-
}
107-
}
108-
}
109-
.zIndex(2)
110-
.onAppear {
111-
self.language = detectLanguage(fileURL: fileURL) ?? .default
112-
self.theme = colorScheme == .dark ? .dark : .light
113-
}
58+
StatusBar(
59+
fileURL: fileURL,
60+
document: $document,
61+
wrapLines: $wrapLines,
62+
useSystemCursor: $useSystemCursor,
63+
cursorPositions: $cursorPositions,
64+
isInLongParse: $isInLongParse,
65+
language: $language,
66+
theme: $theme,
67+
showMinimap: $showMinimap,
68+
indentOption: $indentOption
69+
)
11470
}
11571
.ignoresSafeArea()
11672
.frame(maxWidth: .infinity, maxHeight: .infinity)
@@ -133,32 +89,6 @@ struct ContentView: View {
13389
}
13490
}
13591
}
136-
137-
private func detectLanguage(fileURL: URL?) -> CodeLanguage? {
138-
guard let fileURL else { return nil }
139-
return CodeLanguage.detectLanguageFrom(
140-
url: fileURL,
141-
prefixBuffer: document.text.getFirstLines(5),
142-
suffixBuffer: document.text.getLastLines(5)
143-
)
144-
}
145-
146-
/// Create a label string for cursor positions.
147-
/// - Parameter cursorPositions: The cursor positions to create the label for.
148-
/// - Returns: A string describing the user's location in a document.
149-
func getLabel(_ cursorPositions: [CursorPosition]) -> String {
150-
if cursorPositions.isEmpty {
151-
return "No cursor"
152-
}
153-
154-
// More than one selection, display the number of selections.
155-
if cursorPositions.count > 1 {
156-
return "\(cursorPositions.count) selected ranges"
157-
}
158-
159-
// When there's a single cursor, display the line and column.
160-
return "Line: \(cursorPositions[0].line) Col: \(cursorPositions[0].column) Range: \(cursorPositions[0].range)"
161-
}
16292
}
16393

16494
#Preview {
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
//
2+
// StatusBar.swift
3+
// CodeEditSourceEditorExample
4+
//
5+
// Created by Khan Winter on 4/17/25.
6+
//
7+
8+
import SwiftUI
9+
import CodeEditSourceEditor
10+
import CodeEditLanguages
11+
12+
struct StatusBar: View {
13+
let fileURL: URL?
14+
15+
@Environment(\.colorScheme)
16+
var colorScheme
17+
18+
@Binding var document: CodeEditSourceEditorExampleDocument
19+
@Binding var wrapLines: Bool
20+
@Binding var useSystemCursor: Bool
21+
@Binding var cursorPositions: [CursorPosition]
22+
@Binding var isInLongParse: Bool
23+
@Binding var language: CodeLanguage
24+
@Binding var theme: EditorTheme
25+
@Binding var showMinimap: Bool
26+
@Binding var indentOption: IndentOption
27+
28+
var body: some View {
29+
HStack {
30+
Menu {
31+
Toggle("Wrap Lines", isOn: $wrapLines)
32+
Toggle("Show Minimap", isOn: $showMinimap)
33+
if #available(macOS 14, *) {
34+
Toggle("Use System Cursor", isOn: $useSystemCursor)
35+
} else {
36+
Toggle("Use System Cursor", isOn: $useSystemCursor)
37+
.disabled(true)
38+
.help("macOS 14 required")
39+
}
40+
} label: {}
41+
.background {
42+
Image(systemName: "switch.2")
43+
.foregroundStyle(.secondary)
44+
.font(.system(size: 13.5, weight: .regular))
45+
}
46+
.menuStyle(.borderlessButton)
47+
.menuIndicator(.hidden)
48+
.frame(maxWidth: 18, alignment: .center)
49+
50+
Spacer()
51+
52+
Group {
53+
if isInLongParse {
54+
HStack(spacing: 5) {
55+
ProgressView()
56+
.controlSize(.small)
57+
Text("Parsing Document")
58+
}
59+
} else {
60+
Text(getLabel(cursorPositions))
61+
}
62+
}
63+
.foregroundStyle(.secondary)
64+
Divider()
65+
.frame(height: 12)
66+
LanguagePicker(language: $language)
67+
.buttonStyle(.borderless)
68+
IndentPicker(indentOption: $indentOption, enabled: document.text.isEmpty)
69+
.buttonStyle(.borderless)
70+
}
71+
.font(.subheadline)
72+
.fontWeight(.medium)
73+
.controlSize(.small)
74+
.padding(.horizontal, 8)
75+
.frame(height: 28)
76+
.background(.bar)
77+
.overlay(alignment: .top) {
78+
VStack {
79+
Divider()
80+
.overlay {
81+
if colorScheme == .dark {
82+
Color.black
83+
}
84+
}
85+
}
86+
}
87+
.zIndex(2)
88+
.onAppear {
89+
self.language = detectLanguage(fileURL: fileURL) ?? .default
90+
self.theme = colorScheme == .dark ? .dark : .light
91+
}
92+
}
93+
94+
private func detectLanguage(fileURL: URL?) -> CodeLanguage? {
95+
guard let fileURL else { return nil }
96+
return CodeLanguage.detectLanguageFrom(
97+
url: fileURL,
98+
prefixBuffer: document.text.getFirstLines(5),
99+
suffixBuffer: document.text.getLastLines(5)
100+
)
101+
}
102+
103+
/// Create a label string for cursor positions.
104+
/// - Parameter cursorPositions: The cursor positions to create the label for.
105+
/// - Returns: A string describing the user's location in a document.
106+
func getLabel(_ cursorPositions: [CursorPosition]) -> String {
107+
if cursorPositions.isEmpty {
108+
return "No cursor"
109+
}
110+
111+
// More than one selection, display the number of selections.
112+
if cursorPositions.count > 1 {
113+
return "\(cursorPositions.count) selected ranges"
114+
}
115+
116+
// When there's a single cursor, display the line and column.
117+
return "Line: \(cursorPositions[0].line) Col: \(cursorPositions[0].column) Range: \(cursorPositions[0].range)"
118+
}
119+
}

Package.resolved

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@
1414
"kind" : "remoteSourceControl",
1515
"location" : "https://github.com/CodeEditApp/CodeEditTextView.git",
1616
"state" : {
17-
"revision" : "337b05f22f381f020ab188d3765767e19556d78c",
18-
"version" : "0.9.0"
17+
"revision" : "a5912e60f6bac25cd1cdf8bb532e1125b21cf7f7",
18+
"version" : "0.10.1"
1919
}
2020
},
2121
{
@@ -36,6 +36,15 @@
3636
"version" : "1.1.4"
3737
}
3838
},
39+
{
40+
"identity" : "swift-custom-dump",
41+
"kind" : "remoteSourceControl",
42+
"location" : "https://github.com/pointfreeco/swift-custom-dump",
43+
"state" : {
44+
"revision" : "82645ec760917961cfa08c9c0c7104a57a0fa4b1",
45+
"version" : "1.3.3"
46+
}
47+
},
3948
{
4049
"identity" : "swiftlintplugin",
4150
"kind" : "remoteSourceControl",
@@ -80,6 +89,15 @@
8089
"revision" : "d97db6d63507eb62c536bcb2c4ac7d70c8ec665e",
8190
"version" : "0.23.2"
8291
}
92+
},
93+
{
94+
"identity" : "xctest-dynamic-overlay",
95+
"kind" : "remoteSourceControl",
96+
"location" : "https://github.com/pointfreeco/xctest-dynamic-overlay",
97+
"state" : {
98+
"revision" : "39de59b2d47f7ef3ca88a039dff3084688fe27f4",
99+
"version" : "1.5.2"
100+
}
83101
}
84102
],
85103
"version" : 2

Package.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ let package = Package(
1717
// A fast, efficient, text view for code.
1818
.package(
1919
url: "https://github.com/CodeEditApp/CodeEditTextView.git",
20-
from: "0.9.1"
20+
from: "0.10.1"
2121
),
2222
// tree-sitter languages
2323
.package(

0 commit comments

Comments
 (0)