Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Full Quick Look Support. #1815

Closed
wants to merge 6 commits into from
Closed
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
6 changes: 5 additions & 1 deletion CodeEdit.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@
30E6D0012A6E505200A58B20 /* NavigatorSidebarViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30E6D0002A6E505200A58B20 /* NavigatorSidebarViewModel.swift */; };
3E0196732A3921AC002648D8 /* codeedit_shell_integration_rc.zsh in Resources */ = {isa = PBXBuildFile; fileRef = 3E0196722A3921AC002648D8 /* codeedit_shell_integration_rc.zsh */; };
3E01967A2A392B45002648D8 /* codeedit_shell_integration.bash in Resources */ = {isa = PBXBuildFile; fileRef = 3E0196792A392B45002648D8 /* codeedit_shell_integration.bash */; };
4ACC3CF72C898EA700380885 /* QuickLookPreviewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ACC3CF62C898EA700380885 /* QuickLookPreviewController.swift */; };
4E7F066629602E7B00BB3C12 /* CodeEditSplitViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E7F066529602E7B00BB3C12 /* CodeEditSplitViewController.swift */; };
4EE96ECB2960565E00FFBEA8 /* DocumentsUnitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EE96ECA2960565E00FFBEA8 /* DocumentsUnitTests.swift */; };
4EE96ECE296059E000FFBEA8 /* NSHapticFeedbackPerformerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EE96ECD296059E000FFBEA8 /* NSHapticFeedbackPerformerMock.swift */; };
Expand Down Expand Up @@ -770,6 +771,7 @@
30E6D0002A6E505200A58B20 /* NavigatorSidebarViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigatorSidebarViewModel.swift; sourceTree = "<group>"; };
3E0196722A3921AC002648D8 /* codeedit_shell_integration_rc.zsh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = codeedit_shell_integration_rc.zsh; sourceTree = "<group>"; };
3E0196792A392B45002648D8 /* codeedit_shell_integration.bash */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; path = codeedit_shell_integration.bash; sourceTree = "<group>"; };
4ACC3CF62C898EA700380885 /* QuickLookPreviewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickLookPreviewController.swift; sourceTree = "<group>"; };
4E7F066529602E7B00BB3C12 /* CodeEditSplitViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodeEditSplitViewController.swift; sourceTree = "<group>"; };
4EE96ECA2960565E00FFBEA8 /* DocumentsUnitTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DocumentsUnitTests.swift; sourceTree = "<group>"; };
4EE96ECD296059E000FFBEA8 /* NSHapticFeedbackPerformerMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSHapticFeedbackPerformerMock.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1449,6 +1451,7 @@
285FEC6C27FE4AC700E57D53 /* OutlineView */ = {
isa = PBXGroup;
children = (
4ACC3CF62C898EA700380885 /* QuickLookPreviewController.swift */,
2847019D27FDDF7600F87B6B /* ProjectNavigatorOutlineView.swift */,
285FEC6D27FE4B4A00E57D53 /* ProjectNavigatorViewController.swift */,
6CC17B522C43314000834E2C /* ProjectNavigatorViewController+NSOutlineViewDelegate.swift */,
Expand Down Expand Up @@ -4191,6 +4194,7 @@
B62AEDDC2A27C1B3009A9F52 /* OSLogType+Color.swift in Sources */,
587B9E6329301D8F00AC7927 /* GitLabAccount.swift in Sources */,
6C1CC99B2B1E7CBC0002349B /* FindNavigatorIndexBar.swift in Sources */,
4ACC3CF72C898EA700380885 /* QuickLookPreviewController.swift in Sources */,
285FEC7027FE4B9800E57D53 /* ProjectNavigatorTableViewCell.swift in Sources */,
B69D3EE32C5F536B005CF43A /* ActiveTaskView.swift in Sources */,
300051672BBD3A5D00A98562 /* ServiceContainer.swift in Sources */,
Expand Down Expand Up @@ -5632,7 +5636,7 @@
repositoryURL = "https://github.com/groue/GRDB.swift.git";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 6.0.0;
minimumVersion = 6.29.2;
};
};
6C6BD6F229CD142C00235D17 /* XCRemoteSwiftPackageReference "collectionconcurrencykit" */ = {
Expand Down
3 changes: 3 additions & 0 deletions CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFile.swift
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,9 @@ final class CEWorkspaceFile: Codable, Comparable, Hashable, Identifiable, Editor
FileIcon.iconColor(fileType: type)
}

/// Returns whether the item is being opened in Quick Look.
var isOpeningInQuickLook = false

init(
id: String,
url: URL,
Expand Down
29 changes: 17 additions & 12 deletions CodeEdit/Features/Editor/Views/EditorAreaFileView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,18 +23,23 @@ struct EditorAreaFileView: View {
var textViewCoordinators: [TextViewCoordinator] = []

@ViewBuilder var editorAreaFileView: some View {
if let utType = codeFile.utType, utType.conforms(to: .text) {
CodeFileView(codeFile: codeFile, textViewCoordinators: textViewCoordinators)
} else {
NonTextFileView(fileDocument: codeFile)
.padding(.top, edgeInsets.top - 1.74)
.padding(.bottom, StatusBarView.height + 1.26)
.modifier(UpdateStatusBarInfo(with: codeFile.fileURL))
.onDisappear {
statusBarViewModel.dimensions = nil
statusBarViewModel.fileSize = nil
}
}
if let document = file.fileDocument {
if file.isOpeningInQuickLook {
AnyFileView(file.url)
.padding(.top, edgeInsets.top - 1.74)
.padding(.bottom, StatusBarView.height + 1.26)
} else if let utType = document.utType, utType.conforms(to: .text) {
CodeFileView(codeFile: document, textViewCoordinators: textViewCoordinators)
} else {
NonTextFileView(fileDocument: document)
.padding(.top, edgeInsets.top - 1.74)
.padding(.bottom, StatusBarView.height + 1.26)
.modifier(UpdateStatusBarInfo(with: document.fileURL))
.onDisappear {
statusBarViewModel.dimensions = nil
statusBarViewModel.fileSize = nil
}
}
}

var body: some View {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,9 @@ final class ProjectNavigatorMenu: NSMenu {
return ([.none()], [])
}
var primaryItems = [NSMenuItem]()
if type.conforms(to: .sourceCode) {
primaryItems.append(.sourceCode())
if type.conforms(to: .text) || checkFileIsText(for: item) {
primaryItems.append(menuItem("Source Code", action: #selector(openInSourceCode)))
primaryItems.append(.separator())
}
if type.conforms(to: .propertyList) {
primaryItems.append(.propertyList())
Expand All @@ -141,10 +142,7 @@ final class ProjectNavigatorMenu: NSMenu {
secondaryItems.append(.hex())
}

// FIXME: Update the quickLook condition
if type.conforms(to: .data) {
secondaryItems.append(.quickLook())
}
secondaryItems.append(QuickLookPreviewController.quickLookMenu(item: item, workspace: workspace))

return (primaryItems, secondaryItems)
}
Expand Down Expand Up @@ -178,6 +176,31 @@ final class ProjectNavigatorMenu: NSMenu {
return sourceControlMenu
}

/// Checks if the given `CEWorkspaceFile` is a text file.
/// This function verifies if the content of the file can be decoded as text.
/// It is used as a fallback when the type identifier does not conform to the expected text type.
func checkFileIsText(for item: CEWorkspaceFile) -> Bool {
do {
// Read the first 512 bytes of the file.
let fileHandle = try FileHandle(forReadingFrom: item.url)
let fileData = fileHandle.readData(ofLength: 512)
defer { fileHandle.closeFile() }

// Extreme use case: Returns true for empty files.
// E.g. The user hasn't typed anything on it, yet.
// This is used for files like .env, .gitignore or files that lack an extension.
if fileData.isEmpty {
return true
}

// swiftlint:disable:next non_optional_string_data_conversion
return String(data: fileData, encoding: .utf8) != nil
} catch {
print("Error reading file: \(error)")
return false
}
}

/// Updates the menu for the selected item and hides it if no item is provided.
override func update() {
removeAllItems()
Expand Down Expand Up @@ -264,6 +287,16 @@ final class ProjectNavigatorMenu: NSMenu {
guard let item else { return }
workspace?.workspaceFileManager?.duplicate(file: item)
}

/// Action that opens the file in Source Code.
@objc
private func openInSourceCode() {
guard let item else { return }
item.isOpeningInQuickLook = false
guard let workspace,
let editorManager = workspace.editorManager else { return }
editorManager.openTab(item: item)
}
}

extension NSMenuItem {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
//
// QuickLookPreviewController.swift
// CodeEdit
//
// Created by Leonardo Larrañaga on 7/10/24.
//

import QuickLookUI
import SwiftUI

/// A class that handles file preview using Quick Look.
class QuickLookPreviewController: NSObject, QLPreviewPanelDataSource, QLPreviewPanelDelegate {
/// Shared instance of `QuickLookPreviewController` for global use.
static let shared = QuickLookPreviewController()

/// URL of the item to preview.
static var previewItemURL: URL!

/// Returns the number of items to preview in the Quick Look panel.
/// Requiered function for `QLPreviewPanelDelegate`.
func numberOfPreviewItems(in panel: QLPreviewPanel!) -> Int {
return 1
}

/// Returns the item to preview at the specified index.
/// Requiered function for `QLPreviewPanelDelegate`.
func previewPanel(_ panel: QLPreviewPanel!, previewItemAt index: Int) -> (any QLPreviewItem)! {
return QuickLookPreviewController.previewItemURL as QLPreviewItem
}

/// Opens the item in a Quick Look tab.
@objc
func openQuickLookTab(_ sender: NSMenuItem) {
guard let context = sender.representedObject as? (CEWorkspaceFile, WorkspaceDocument?) else { return }
let (item, workspace) = context

guard let workspace,
let editorManager = workspace.editorManager else { return }
item.isOpeningInQuickLook = true
editorManager.openTab(item: item)
}

/// Creates a Quick Look menu for the specified item.
/// - Parameter item: The workspace file (`CEWorkspaceFile`) for which the menu item will be created.
/// - Returns: A menu item configured to open Quick Look.
static func quickLookMenu(item: CEWorkspaceFile, workspace: WorkspaceDocument?) -> NSMenuItem {
QuickLookPreviewController.previewItemURL = item.url

let quickLookMenuItem = NSMenuItem(
title: "Quick Look",
action: #selector(QuickLookPreviewController.shared.openQuickLookTab(_:)),
keyEquivalent: ""
)
quickLookMenuItem.target = QuickLookPreviewController.shared
quickLookMenuItem.representedObject = (item, workspace)

return quickLookMenuItem
}
}