Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 35 additions & 23 deletions CodeEdit.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

10 changes: 5 additions & 5 deletions CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFile.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,11 @@ import Combine
/// loading all intermediate subdirectories (from the nearest cached parent to the file) has not been done yet and doing
/// so would be unnecessary.
///
/// An example of this is in the ``QuickOpenView``. This view finds a file URL via a search bar, and needs to display a
/// quick preview of the file. There's a good chance the file is deep in some subdirectory of the workspace, so fetching
/// it from the ``CEWorkspaceFileManager`` may require loading and caching multiple directories. Instead, it just
/// makes a disconnected object and uses it for the preview. Then, when opening the file in the workspace it forces the
/// file to be loaded and cached.
/// An example of this is in the ``OpenQuicklyView``. This view finds a file URL via a search bar, and needs to display
/// a quick preview of the file. There's a good chance the file is deep in some subdirectory of the workspace, so
/// fetching it from the ``CEWorkspaceFileManager`` may require loading and caching multiple directories. Instead, it
/// just makes a disconnected object and uses it for the preview. Then, when opening the file in the workspace it
/// forces the file to be loaded and cached.
final class CEWorkspaceFile: Codable, Comparable, Hashable, Identifiable, EditorTabRepresentable {

/// The id of the ``CEWorkspaceFile``.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
// Created by Alex on 25.05.2022.
//

import Foundation
import SwiftUI

/// Simple state class for command palette view. Contains currently selected command,
/// query text and list of filtered commands
Expand Down Expand Up @@ -35,4 +35,17 @@ final class QuickActionsViewModel: ObservableObject {
self.filteredCommands = CommandManager.shared.commands.filter { $0.title.localizedCaseInsensitiveContains(val) }
self.selected = self.filteredCommands.first
}

func highlight(_ commandTitle: String) -> NSAttributedString {
let attribText = NSMutableAttributedString(string: commandTitle)
let range: NSRange = attribText.mutableString.range(
of: self.commandQuery,
options: NSString.CompareOptions.caseInsensitive
)
attribText.addAttribute(.foregroundColor, value: NSColor(Color(.labelColor)), range: range)
attribText.addAttribute(.font, value: NSFont.boldSystemFont(ofSize: NSFont.systemFontSize), range: range)

return attribText
}

}
52 changes: 6 additions & 46 deletions CodeEdit/Features/Commands/Views/QuickActionsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,15 +43,19 @@ struct QuickActionsView: View {
}

var body: some View {
SearchPanelView<SearchResultLabel, EmptyView, Command>(
SearchPanelView<QuickSearchResultLabel, EmptyView, Command>(
title: "Commands",
image: Image(systemName: "magnifyingglass"),
options: $state.filteredCommands,
text: $state.commandQuery,
alwaysShowOptions: true,
optionRowHeight: 30
) { command in
SearchResultLabel(labelName: command.title, textToMatch: state.commandQuery)
QuickSearchResultLabel(
labelName: command.title,
charactersToHighlight: [],
nsLabelName: state.highlight(command.title)
)
} onRowClick: { command in
callHandler(command: command)
} onClose: {
Expand All @@ -62,47 +66,3 @@ struct QuickActionsView: View {
}
}
}

/// Implementation of command palette entity. While swiftui does not allow to use NSMutableAttributeStrings,
/// the only way to fallback to UIKit and have NSViewRepresentable to be a bridge between UIKit and SwiftUI.
/// Highlights currently entered text query

struct SearchResultLabel: NSViewRepresentable {

var labelName: String
var textToMatch: String

public func makeNSView(context: Context) -> some NSTextField {
let label = NSTextField(wrappingLabelWithString: labelName)
label.translatesAutoresizingMaskIntoConstraints = false
label.drawsBackground = false
label.textColor = .labelColor
label.isEditable = false
label.isSelectable = false
label.font = .labelFont(ofSize: 13)
label.allowsDefaultTighteningForTruncation = false
label.cell?.truncatesLastVisibleLine = true
label.cell?.wraps = true
label.maximumNumberOfLines = 1
label.attributedStringValue = highlight()
return label
}

func highlight() -> NSAttributedString {
let attribText = NSMutableAttributedString(string: self.labelName)
let range: NSRange = attribText.mutableString.range(
of: self.textToMatch,
options: NSString.CompareOptions.caseInsensitive
)
attribText.addAttribute(.foregroundColor, value: NSColor(Color(.labelColor)), range: range)
attribText.addAttribute(.font, value: NSFont.boldSystemFont(ofSize: NSFont.systemFontSize), range: range)

return attribText
}

func updateNSView(_ nsView: NSViewType, context: Context) {
nsView.textColor = textToMatch.isEmpty ? .labelColor : .secondaryLabelColor
nsView.attributedStringValue = highlight()
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ final class CodeEditWindowController: NSWindowController, NSToolbarDelegate, Obs
}

@IBAction func openQuickly(_ sender: Any) {
if let workspace, let state = workspace.quickOpenViewModel {
if let workspace, let state = workspace.openQuicklyViewModel {
if let quickOpenPanel {
if quickOpenPanel.isKeyWindow {
quickOpenPanel.close()
Expand All @@ -139,7 +139,7 @@ final class CodeEditWindowController: NSWindowController, NSToolbarDelegate, Obs
let panel = SearchPanel()
self.quickOpenPanel = panel

let contentView = QuickOpenView(state: state) {
let contentView = OpenQuicklyView(state: state) {
panel.close()
} openFile: { file in
workspace.editorManager.openTab(item: file)
Expand Down
4 changes: 2 additions & 2 deletions CodeEdit/Features/Documents/WorkspaceDocument.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ final class WorkspaceDocument: NSDocument, ObservableObject, NSToolbarDelegate {
var statusBarViewModel = StatusBarViewModel()
var utilityAreaModel = UtilityAreaViewModel()
var searchState: SearchState?
var quickOpenViewModel: QuickOpenViewModel?
var openQuicklyViewModel: OpenQuicklyViewModel?
var commandsPaletteState: QuickActionsViewModel?
var listenerModel: WorkspaceNotificationModel = .init()
var sourceControlManager: SourceControlManager?
Expand Down Expand Up @@ -123,7 +123,7 @@ final class WorkspaceDocument: NSDocument, ObservableObject, NSToolbarDelegate {
self.sourceControlManager = sourceControlManager
sourceControlManager.fileManager = workspaceFileManager
self.searchState = .init(self)
self.quickOpenViewModel = .init(fileURL: url)
self.openQuicklyViewModel = .init(fileURL: url)
self.commandsPaletteState = .init()

editorManager.restoreFromState(self)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
//
// OpenQuicklyViewModel.swift
// CodeEditModules/QuickOpen
//
// Created by Marco Carnevali on 05/04/22.
//

import Combine
import Foundation
import CollectionConcurrencyKit

final class OpenQuicklyViewModel: ObservableObject {
@Published var query: String = ""
@Published var searchResults: [SearchResult] = []

let fileURL: URL
var runningTask: Task<Void, Never>?

init(fileURL: URL) {
self.fileURL = fileURL
}

/// This is used to populate the ``OpenQuicklyListItemView`` view which shows the search results to the user.
///
/// ``OpenQuicklyPreviewView`` also uses this to load the `fileUrl` for preview.
struct SearchResult: Identifiable, Hashable {
var id: String { fileURL.id }
let fileURL: URL
let matchedCharacters: [NSRange]

// This custom Hashable implementation prevents the highlighted
// selection from flickering when searching in 'Open Quickly'.
//
// See https://github.com/CodeEditApp/CodeEdit/pull/1790#issuecomment-2206832901
// for flickering visuals.
//
// Before commit 0e28b382f59184b7ebe5a7c3295afa3655b7d4e7, only the fileURL
// was retrieved from the search results and it worked as expected.
//
static func == (lhs: Self, rhs: Self) -> Bool { lhs.fileURL == rhs.fileURL }
func hash(into hasher: inout Hasher) { hasher.combine(fileURL) }
}

func fetchResults() {
let startTime = Date()
guard query != "" else {
searchResults = []
return
}

runningTask?.cancel()
runningTask = Task.detached(priority: .userInitiated) {
let enumerator = FileManager.default.enumerator(
at: self.fileURL,
includingPropertiesForKeys: [
.isRegularFileKey
],
options: [
.skipsPackageDescendants
]
)
if let filePaths = enumerator?.allObjects as? [URL] {
guard !Task.isCancelled else { return }
/// removes all filePaths which aren't regular files
let filteredFiles = filePaths.filter { url in
do {
let values = try url.resourceValues(forKeys: [.isRegularFileKey])
return (values.isRegularFile ?? false)
} catch {
return false
}
}

let fuzzySearchResults = await filteredFiles.fuzzySearch(
query: self.query.trimmingCharacters(in: .whitespaces)
).concurrentMap {
SearchResult(
fileURL: $0.item,
matchedCharacters: $0.result.matchedParts
)
}

guard !Task.isCancelled else { return }
await MainActor.run {
self.searchResults = fuzzySearchResults
print("Duration: \(Date().timeIntervalSince(startTime))")
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,37 +1,39 @@
//
// QuickOpenItem.swift
// OpenQuicklyListItemView.swift
// CodeEditModules/QuickOpen
//
// Created by Pavel Kasila on 20.03.22.
//

import SwiftUI

struct QuickOpenItem: View {
struct OpenQuicklyListItemView: View {
private let baseDirectory: URL
private let fileURL: URL
private let searchResult: OpenQuicklyViewModel.SearchResult

init(
baseDirectory: URL,
fileURL: URL
searchResult: OpenQuicklyViewModel.SearchResult
) {
self.baseDirectory = baseDirectory
self.fileURL = fileURL
self.searchResult = searchResult
}

var relativePathComponents: ArraySlice<String> {
return fileURL.pathComponents.dropFirst(baseDirectory.pathComponents.count).dropLast()
return searchResult.fileURL.pathComponents.dropFirst(baseDirectory.pathComponents.count).dropLast()
}

var body: some View {
HStack(spacing: 8) {
Image(nsImage: NSWorkspace.shared.icon(forFile: fileURL.path))
Image(nsImage: NSWorkspace.shared.icon(forFile: searchResult.fileURL.path))
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 24, height: 24)
VStack(alignment: .leading, spacing: 0) {
Text(fileURL.lastPathComponent).font(.system(size: 13))
.lineLimit(1)
QuickSearchResultLabel(
labelName: searchResult.fileURL.lastPathComponent,
charactersToHighlight: searchResult.matchedCharacters
)
Text(relativePathComponents.joined(separator: " ▸ "))
.font(.system(size: 10.5))
.foregroundColor(.secondary)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
//
// QuickOpenPreviewView.swift
// OpenQuicklyPreviewView.swift
// CodeEditModules/QuickOpen
//
// Created by Pavel Kasila on 20.03.22.
//

import SwiftUI

struct QuickOpenPreviewView: View {
struct OpenQuicklyPreviewView: View {

private let queue = DispatchQueue(label: "app.codeedit.CodeEdit.quickOpen.preview")
private let item: CEWorkspaceFile
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// QuickOpenView.swift
// OpenQuicklyView.swift
// CodeEditModules/QuickOpen
//
// Created by Pavel Kasila on 20.03.22.
Expand All @@ -13,22 +13,22 @@ extension URL: Identifiable {
}
}

struct QuickOpenView: View {
struct OpenQuicklyView: View {
@EnvironmentObject private var workspace: WorkspaceDocument

private let onClose: () -> Void
private let openFile: (CEWorkspaceFile) -> Void

@ObservedObject private var state: QuickOpenViewModel
@ObservedObject private var openQuicklyViewModel: OpenQuicklyViewModel

@State private var selectedItem: CEWorkspaceFile?

init(
state: QuickOpenViewModel,
state: OpenQuicklyViewModel,
onClose: @escaping () -> Void,
openFile: @escaping (CEWorkspaceFile) -> Void
) {
self.state = state
self.openQuicklyViewModel = state
self.onClose = onClose
self.openFile = openFile
}
Expand All @@ -37,28 +37,31 @@ struct QuickOpenView: View {
SearchPanelView(
title: "Open Quickly",
image: Image(systemName: "magnifyingglass"),
options: $state.openQuicklyFiles,
text: $state.openQuicklyQuery,
options: $openQuicklyViewModel.searchResults,
text: $openQuicklyViewModel.query,
optionRowHeight: 40
) { file in
QuickOpenItem(baseDirectory: state.fileURL, fileURL: file)
} preview: { fileURL in
QuickOpenPreviewView(item: CEWorkspaceFile(url: fileURL))
} onRowClick: { fileURL in
) { searchResult in
OpenQuicklyListItemView(
baseDirectory: openQuicklyViewModel.fileURL,
searchResult: searchResult
)
} preview: { searchResult in
OpenQuicklyPreviewView(item: CEWorkspaceFile(url: searchResult.fileURL))
} onRowClick: { searchResult in
guard let file = workspace.workspaceFileManager?.getFile(
fileURL.relativePath,
searchResult.fileURL.relativePath,
createIfNotFound: true
) else {
return
}
openFile(file)
state.openQuicklyQuery = ""
openQuicklyViewModel.query = ""
onClose()
} onClose: {
onClose()
}
.onReceive(state.$openQuicklyQuery.debounce(for: 0.2, scheduler: DispatchQueue.main)) { _ in
state.fetchOpenQuickly()
.onReceive(openQuicklyViewModel.$query.debounce(for: 0.2, scheduler: DispatchQueue.main)) { _ in
openQuicklyViewModel.fetchResults()
}
}
}
Loading