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

LSP Activates On Workspace Files, LSP Document Open/Close #1879

Merged
merged 10 commits into from
Sep 18, 2024
147 changes: 105 additions & 42 deletions CodeEdit.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

13 changes: 7 additions & 6 deletions CodeEdit/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -226,13 +226,14 @@ final class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject {
func documentController(_ docController: NSDocumentController, didCloseAll: Bool, contextInfo: Any) {
NSApplication.shared.reply(toApplicationShouldTerminate: didCloseAll)
}
}

/// Setup all the services into a ServiceContainer for the application to use.
private func setupServiceContainer() {
ServiceContainer.register(
LSPService()
)
/// Setup all the services into a ServiceContainer for the application to use.
@MainActor
private func setupServiceContainer() {
ServiceContainer.register(
LSPService()
)
}
}

extension AppDelegate {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,48 +44,59 @@ extension CEWorkspaceFileManager {
/// - Parameters:
/// - fileName: The name of the new file
/// - file: The file to add the new file to.
/// - useExtension: The file extension to use. Leave `nil` to guess using relevant nearby files.
/// - Authors: Mattijs Eikelenboom, KaiTheRedNinja. *Moved from 7c27b1e*
func addFile(fileName: String, toFile file: CEWorkspaceFile) {
/// - Throws: Throws a `CocoaError.fileWriteUnknown` with the file url if creating the file fails, and calls
/// ``rebuildFiles(fromItem:deep:)`` which throws other `FileManager` errors.
func addFile(fileName: String, toFile file: CEWorkspaceFile, useExtension: String? = nil) throws {
// check the folder for other files, and see what the most common file extension is
var fileExtensions: [String: Int] = ["": 0]

for child in (
file.isFolder ? file.flattenedSiblings(withHeight: 2, ignoringFolders: true, using: self)
: file.parent?.flattenedSiblings(withHeight: 2, ignoringFolders: true, using: self)
) ?? []
where !child.isFolder {
// if the file extension was present before, add it now
let childFileName = child.fileName(typeHidden: false)
if let index = childFileName.lastIndex(of: ".") {
let childFileExtension = ".\(childFileName.suffix(from: index).dropFirst())"
fileExtensions[childFileExtension] = (fileExtensions[childFileExtension] ?? 0) + 1
} else {
fileExtensions[""] = (fileExtensions[""] ?? 0) + 1
var fileExtension: String
if let useExtension {
fileExtension = useExtension
} else {
var fileExtensions: [String: Int] = ["": 0]

for child in (
file.isFolder ? file.flattenedSiblings(withHeight: 2, ignoringFolders: true, using: self)
: file.parent?.flattenedSiblings(withHeight: 2, ignoringFolders: true, using: self)
) ?? []
where !child.isFolder {
// if the file extension was present before, add it now
let childFileName = child.fileName(typeHidden: false)
if let index = childFileName.lastIndex(of: ".") {
let childFileExtension = ".\(childFileName.suffix(from: index).dropFirst())"
fileExtensions[childFileExtension] = (fileExtensions[childFileExtension] ?? 0) + 1
} else {
fileExtensions[""] = (fileExtensions[""] ?? 0) + 1
}
}

fileExtension = fileExtensions.sorted(by: { $0.value > $1.value }).first?.key ?? "txt"
}

var largestValue = 0
var idealExtension = ""
for (extName, count) in fileExtensions where count > largestValue {
idealExtension = extName
largestValue = count
if !fileExtension.starts(with: ".") {
fileExtension = "." + fileExtension
}

var fileUrl = file.nearestFolder.appendingPathComponent("\(fileName)\(idealExtension)")
var fileUrl = file.nearestFolder.appendingPathComponent("\(fileName)\(fileExtension)")
// If a file/folder with the same name exists, add a number to the end.
var fileNumber = 0
while fileManager.fileExists(atPath: fileUrl.path) {
fileNumber += 1
fileUrl = fileUrl.deletingLastPathComponent()
.appendingPathComponent("\(fileName)\(fileNumber)\(idealExtension)")
.appendingPathComponent("\(fileName)\(fileNumber)\(fileExtension)")
}

// Create the file
fileManager.createFile(
guard fileManager.createFile(
atPath: fileUrl.path,
contents: nil,
attributes: [FileAttributeKey.creationDate: Date()]
)
) else {
throw CocoaError.error(.fileWriteUnknown, url: fileUrl)
}

try rebuildFiles(fromItem: file)
}

/// This function deletes the item or folder from the current project by moving to Trash
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -189,8 +189,9 @@ final class CEWorkspaceFileManager {
/// - Parameter file: The parent element.
/// - Returns: A child element with an associated parent.
func createChild(_ url: URL, forParent file: CEWorkspaceFile) -> CEWorkspaceFile {
let childId = URL(filePath: file.id).appendingPathComponent(url.lastPathComponent).relativePath
let newFileItem = CEWorkspaceFile(id: childId, url: url)
let relativeURL = URL(filePath: file.id).appendingPathComponent(url.lastPathComponent)
let childId = relativeURL.relativePath
let newFileItem = CEWorkspaceFile(id: childId, url: relativeURL)
newFileItem.parent = file
return newFileItem
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ final class CodeFileDocument: NSDocument, ObservableObject {

static let logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "", category: "CodeFileDocument")

@Service var lspService: LSPService

/// The text content of the document, stored as a text storage
///
/// This is intentionally not a `@Published` variable. If it were published, SwiftUI would do a string
Expand Down Expand Up @@ -75,6 +77,9 @@ final class CodeFileDocument: NSDocument, ObservableObject {
return type
}

/// A stable string to use when identifying documents with language servers.
var languageServerURI: String? { fileURL?.languageServerURI }

/// Specify options for opening the file such as the initial cursor positions.
/// Nulled by ``CodeFileView`` on first load.
var openOptions: OpenOptions?
Expand Down Expand Up @@ -173,4 +178,33 @@ final class CodeFileDocument: NSDocument, ObservableObject {

self.isDocumentEditedSubject.send(self.isDocumentEdited)
}

override func close() {
super.close()
lspService.closeDocument(self)
}

func getLanguage() -> CodeLanguage {
guard let url = fileURL else {
return .default
}
return language ?? CodeLanguage.detectLanguageFrom(
url: url,
prefixBuffer: content?.string.getFirstLines(5),
suffixBuffer: content?.string.getLastLines(5)
)
}

func findWorkspace() -> WorkspaceDocument? {
CodeEditDocumentController.shared.documents.first(where: { doc in
guard let workspace = doc as? WorkspaceDocument, let path = self.languageServerURI else { return false }
// createIfNotFound is safe here because it will still exit if the file and the workspace
// do not share a path prefix
return workspace
.workspaceFileManager?
.getFile(path, createIfNotFound: true)?
.fileDocument?
.isEqual(self) ?? false
}) as? WorkspaceDocument
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ final class CodeEditDocumentController: NSDocumentController {
@Environment(\.openWindow)
private var openWindow

@LazyService var lspService: LSPService

private let fileManager = FileManager.default

override func newDocument(_ sender: Any?) {
Expand Down Expand Up @@ -79,6 +81,10 @@ final class CodeEditDocumentController: NSDocumentController {
override func removeDocument(_ document: NSDocument) {
super.removeDocument(document)

if let workspace = document as? WorkspaceDocument, let path = workspace.fileURL?.absoluteURL.path() {
lspService.closeWorkspace(path)
}

if CodeEditDocumentController.shared.documents.isEmpty {
switch Settings[\.general].reopenWindowAfterClose {
case .showWelcomeWindow:
Expand All @@ -96,6 +102,13 @@ final class CodeEditDocumentController: NSDocumentController {
super.clearRecentDocuments(sender)
UserDefaults.standard.set([Any](), forKey: "recentProjectPaths")
}

override func addDocument(_ document: NSDocument) {
super.addDocument(document)
if let document = document as? CodeFileDocument {
lspService.openDocument(document)
}
}
}

extension NSDocumentController {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import LanguageServerProtocol

@objc(WorkspaceDocument)
final class WorkspaceDocument: NSDocument, ObservableObject, NSToolbarDelegate {
@Service var lspService: LSPService
thecoolwinter marked this conversation as resolved.
Show resolved Hide resolved

@Published var sortFoldersOnTop: Bool = true

Expand Down
1 change: 0 additions & 1 deletion CodeEdit/Features/Editor/Models/Editor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,6 @@ final class Editor: ObservableObject, Identifiable {
let contentType = item.file.resolvedURL.contentType
let codeFile = try CodeFileDocument(
for: item.file.url,
// TODO: FILE CONTENTS ARE READ MULTIPLE TIMES
withContentsOf: item.file.resolvedURL,
ofType: contentType?.identifier ?? ""
)
Expand Down
13 changes: 1 addition & 12 deletions CodeEdit/Features/Editor/Views/CodeFileView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ struct CodeFileView: View {
var body: some View {
CodeEditSourceEditor(
codeFile.content ?? NSTextStorage(),
language: getLanguage(),
language: codeFile.getLanguage(),
theme: currentTheme.editor.editorTheme,
font: font,
tabWidth: codeFile.defaultTabWidth ?? defaultTabWidth,
Expand Down Expand Up @@ -156,17 +156,6 @@ struct CodeFileView: View {
}
}

private func getLanguage() -> CodeLanguage {
guard let url = codeFile.fileURL else {
return .default
}
return codeFile.language ?? CodeLanguage.detectLanguageFrom(
url: url,
prefixBuffer: codeFile.content?.string.getFirstLines(5),
suffixBuffer: codeFile.content?.string.getLastLines(5)
)
}

private func getBracketPairHighlight() -> BracketPairHighlight? {
let color = if Settings[\.textEditing].bracketHighlight.useCustomColor {
Settings[\.textEditing].bracketHighlight.color.nsColor
Expand Down
34 changes: 13 additions & 21 deletions CodeEdit/Features/Editor/Views/EditorAreaView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
//

import SwiftUI
import CodeEditTextView

struct EditorAreaView: View {
@AppSettings(\.general.showEditorPathBar)
Expand All @@ -25,6 +26,12 @@ struct EditorAreaView: View {

@State var codeFile: CodeFileDocument?

init(editor: Editor, focus: FocusState<Editor?>.Binding) {
self.editor = editor
self._focus = focus
self.codeFile = editor.selectedTab?.file.fileDocument
}

var body: some View {
var shouldShowTabBar: Bool {
return navigationStyle == .openInTabs
Expand Down Expand Up @@ -54,22 +61,6 @@ struct EditorAreaView: View {
.opacity(dimEditorsWithoutFocus && editor != editorManager.activeEditor ? 0.5 : 1)
} else {
LoadingFileView(selected.file.name)
.task {
do {
let contentType = selected.file.resolvedURL.contentType
let newCodeFile = try CodeFileDocument(
for: selected.file.url,
withContentsOf: selected.file.resolvedURL,
ofType: contentType?.identifier ?? ""
)

selected.file.fileDocument = newCodeFile
CodeEditDocumentController.shared.addDocument(newCodeFile)
self.codeFile = newCodeFile
} catch {
print(error.localizedDescription)
}
}
}

} else {
Expand Down Expand Up @@ -108,11 +99,12 @@ struct EditorAreaView: View {
.background(EffectView(.headerView))
}
.focused($focus, equals: editor)
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("CodeEditor.didBeginEditing"))) { _ in
if navigationStyle == .openInTabs {
editor.temporaryTab = nil
}
}
// Fixing this is causing a malloc exception when a file is edited & closed. See #1886
// .onReceive(NotificationCenter.default.publisher(for: TextView.textDidChangeNotification)) { _ in
// if navigationStyle == .openInTabs {
// editor.temporaryTab = nil
// }
// }
.onChange(of: navigationStyle) { newValue in
if newValue == .openInPlace && editor.tabs.count == 1 {
editor.temporaryTab = editor.tabs[0]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
//
// LanguageIdentifier+CodeLanguage.swift
// CodeEdit
//
// Created by Khan Winter on 9/9/24.
//

import LanguageServerProtocol
import CodeEditLanguages

extension CodeLanguage {
var lspLanguage: LanguageIdentifier? {
switch self.id {
case .agda,
.bash,
.haskell,
.julia,
.kotlin,
.ocaml,
.ocamlInterface,
.regex,
.toml,
.verilog,
.zig,
.plainText:
return nil
case .c:
return .c
case .cpp:
return .cpp
case .cSharp:
return .csharp
case .css:
return .css
case .dart:
return .dart
case .dockerfile:
return .dockerfile
case .elixir:
return .elixir
case .go, .goMod:
return .go
case .html:
return .html
case .java:
return .java
case .javascript, .jsdoc:
return .javascript
case .json:
return .json
case .jsx:
return .javascriptreact
case .lua:
return .lua
case .markdown, .markdownInline:
return .markdown
case .objc:
return .objc
case .perl:
return .perl
case .php:
return .php
case .python:
return .python
case .ruby:
return .ruby
case .rust:
return .rust
case .scala:
return .scala
case .sql:
return .sql
case .swift:
return .swift
case .tsx:
return .typescriptreact
case .typescript:
return .typescript
case .yaml:
return .yaml
}
}
}
Loading
Loading