diff --git a/CodeEdit.xcodeproj/project.pbxproj b/CodeEdit.xcodeproj/project.pbxproj index 05cb4bc15..2872fb44b 100644 --- a/CodeEdit.xcodeproj/project.pbxproj +++ b/CodeEdit.xcodeproj/project.pbxproj @@ -64,7 +64,6 @@ 3000516A2BBD3A8200A98562 /* ServiceType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 300051692BBD3A8200A98562 /* ServiceType.swift */; }; 3000516C2BBD3A9500A98562 /* ServiceWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3000516B2BBD3A9500A98562 /* ServiceWrapper.swift */; }; 3026F50F2AC006C80061227E /* InspectorAreaViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3026F50E2AC006C80061227E /* InspectorAreaViewModel.swift */; }; - 30571B762C282592009CD5BB /* LSPCache+Data.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30571B752C282592009CD5BB /* LSPCache+Data.swift */; }; 30AB4EBB2BF718A100ED4431 /* DeveloperSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30AB4EBA2BF718A100ED4431 /* DeveloperSettings.swift */; }; 30AB4EBD2BF71CA800ED4431 /* DeveloperSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30AB4EBC2BF71CA800ED4431 /* DeveloperSettingsView.swift */; }; 30AB4EC22BF7253200ED4431 /* KeyValueTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30AB4EC12BF7253200ED4431 /* KeyValueTable.swift */; }; @@ -78,7 +77,6 @@ 30B088042C0D53080063A882 /* LanguageServer+DocumentHighlight.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30B087E72C0D53080063A882 /* LanguageServer+DocumentHighlight.swift */; }; 30B088052C0D53080063A882 /* LanguageServer+DocumentLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30B087E82C0D53080063A882 /* LanguageServer+DocumentLink.swift */; }; 30B088062C0D53080063A882 /* LanguageServer+DocumentSymbol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30B087E92C0D53080063A882 /* LanguageServer+DocumentSymbol.swift */; }; - 30B088072C0D53080063A882 /* LanguageServer+DocumentUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30B087EA2C0D53080063A882 /* LanguageServer+DocumentUtil.swift */; }; 30B088082C0D53080063A882 /* LanguageServer+FoldingRange.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30B087EB2C0D53080063A882 /* LanguageServer+FoldingRange.swift */; }; 30B088092C0D53080063A882 /* LanguageServer+Formatting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30B087EC2C0D53080063A882 /* LanguageServer+Formatting.swift */; }; 30B0880A2C0D53080063A882 /* LanguageServer+Hover.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30B087ED2C0D53080063A882 /* LanguageServer+Hover.swift */; }; @@ -92,9 +90,8 @@ 30B088122C0D53080063A882 /* LanguageServer+TypeDefinition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30B087F52C0D53080063A882 /* LanguageServer+TypeDefinition.swift */; }; 30B088142C0D53080063A882 /* LanguageServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30B087F72C0D53080063A882 /* LanguageServer.swift */; }; 30B088152C0D53080063A882 /* LSPService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30B087F82C0D53080063A882 /* LSPService.swift */; }; - 30B088162C0D53080063A882 /* LSPCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30B087F92C0D53080063A882 /* LSPCache.swift */; }; 30B088172C0D53080063A882 /* LSPUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30B087FA2C0D53080063A882 /* LSPUtil.swift */; }; - 30CB648D2C12680F00CC8A9E /* LSPEventHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30CB648C2C12680F00CC8A9E /* LSPEventHandler.swift */; }; + 30CB648D2C12680F00CC8A9E /* LSPService+Events.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30CB648C2C12680F00CC8A9E /* LSPService+Events.swift */; }; 30CB64912C16CA8100CC8A9E /* LanguageServerProtocol in Frameworks */ = {isa = PBXBuildFile; productRef = 30CB64902C16CA8100CC8A9E /* LanguageServerProtocol */; }; 30CB64942C16CA9100CC8A9E /* LanguageClient in Frameworks */ = {isa = PBXBuildFile; productRef = 30CB64932C16CA9100CC8A9E /* LanguageClient */; }; 30E6D0012A6E505200A58B20 /* NavigatorSidebarViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30E6D0002A6E505200A58B20 /* NavigatorSidebarViewModel.swift */; }; @@ -419,6 +416,7 @@ 6C6BD70129CD172700235D17 /* ExtensionsListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C6BD70029CD172700235D17 /* ExtensionsListView.swift */; }; 6C6BD70429CD17B600235D17 /* ExtensionsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C6BD70329CD17B600235D17 /* ExtensionsManager.swift */; }; 6C7256D729A3D7D000C2D3E0 /* SplitViewControllerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C7256D629A3D7D000C2D3E0 /* SplitViewControllerView.swift */; }; + 6C7D6D462C9092EC00B69EE0 /* BufferingServerConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C7D6D452C9092EC00B69EE0 /* BufferingServerConnection.swift */; }; 6C7F37FE2A3EA6FA00217B83 /* View+focusedValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C7F37FD2A3EA6FA00217B83 /* View+focusedValue.swift */; }; 6C81916729B3E80700B75C92 /* ModifierKeysObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C81916629B3E80700B75C92 /* ModifierKeysObserver.swift */; }; 6C81916B29B41DD300B75C92 /* DequeModule in Frameworks */ = {isa = PBXBuildFile; productRef = 6C81916A29B41DD300B75C92 /* DequeModule */; }; @@ -449,11 +447,20 @@ 6CC17B4F2C432AE000834E2C /* CodeEditSourceEditor in Frameworks */ = {isa = PBXBuildFile; productRef = 6CC17B4E2C432AE000834E2C /* CodeEditSourceEditor */; }; 6CC17B512C43311900834E2C /* ProjectNavigatorViewController+NSOutlineViewDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CC17B502C43311900834E2C /* ProjectNavigatorViewController+NSOutlineViewDataSource.swift */; }; 6CC17B532C43314000834E2C /* ProjectNavigatorViewController+NSOutlineViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CC17B522C43314000834E2C /* ProjectNavigatorViewController+NSOutlineViewDelegate.swift */; }; - 6CC17B592C43F53700834E2C /* CodeEditSourceEditor in Frameworks */ = {isa = PBXBuildFile; productRef = 6CC17B582C43F53700834E2C /* CodeEditSourceEditor */; }; 6CC17B5B2C44258700834E2C /* WindowControllerPropertyWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CC17B5A2C44258700834E2C /* WindowControllerPropertyWrapper.swift */; }; 6CC9E4B229B5669900C97388 /* Environment+ActiveEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CC9E4B129B5669900C97388 /* Environment+ActiveEditor.swift */; }; 6CD0358A2C3461160091E1F4 /* KeyWindowControllerObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CD035892C3461160091E1F4 /* KeyWindowControllerObserver.swift */; }; 6CD03B6A29FC773F001BD1D0 /* SettingsInjector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CD03B6929FC773F001BD1D0 /* SettingsInjector.swift */; }; + 6CD26C6E2C8EA1E600ADBA38 /* LanguageServerFileMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CD26C6D2C8EA1E600ADBA38 /* LanguageServerFileMap.swift */; }; + 6CD26C772C8EA83900ADBA38 /* URL+LanguageServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CD26C762C8EA83900ADBA38 /* URL+LanguageServer.swift */; }; + 6CD26C7A2C8EA8A500ADBA38 /* LSPCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CD26C782C8EA8A500ADBA38 /* LSPCache.swift */; }; + 6CD26C7B2C8EA8A500ADBA38 /* LSPCache+Data.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CD26C792C8EA8A500ADBA38 /* LSPCache+Data.swift */; }; + 6CD26C7D2C8EA8F400ADBA38 /* LanguageServer+DocumentSync.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CD26C7C2C8EA8F400ADBA38 /* LanguageServer+DocumentSync.swift */; }; + 6CD26C812C8F8A4400ADBA38 /* LanguageIdentifier+CodeLanguage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CD26C802C8F8A4400ADBA38 /* LanguageIdentifier+CodeLanguage.swift */; }; + 6CD26C852C8F907800ADBA38 /* CodeEditSourceEditor in Frameworks */ = {isa = PBXBuildFile; productRef = 6CD26C842C8F907800ADBA38 /* CodeEditSourceEditor */; }; + 6CD26C872C8F90FD00ADBA38 /* LazyServiceWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CD26C862C8F90FD00ADBA38 /* LazyServiceWrapper.swift */; }; + 6CD26C8A2C8F91ED00ADBA38 /* LanguageServer+DocumentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CD26C892C8F91ED00ADBA38 /* LanguageServer+DocumentTests.swift */; }; + 6CD3CA552C8B508200D83DCD /* CodeEditSourceEditor in Frameworks */ = {isa = PBXBuildFile; productRef = 6CD3CA542C8B508200D83DCD /* CodeEditSourceEditor */; }; 6CDA84AD284C1BA000C1CC3A /* EditorTabBarContextMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CDA84AC284C1BA000C1CC3A /* EditorTabBarContextMenu.swift */; }; 6CE21E812C643D8F0031B056 /* CETerminalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CE21E802C643D8F0031B056 /* CETerminalView.swift */; }; 6CE21E872C650D2C0031B056 /* SwiftTerm in Frameworks */ = {isa = PBXBuildFile; productRef = 6CE21E862C650D2C0031B056 /* SwiftTerm */; }; @@ -729,7 +736,6 @@ 300051692BBD3A8200A98562 /* ServiceType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServiceType.swift; sourceTree = ""; }; 3000516B2BBD3A9500A98562 /* ServiceWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServiceWrapper.swift; sourceTree = ""; }; 3026F50E2AC006C80061227E /* InspectorAreaViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InspectorAreaViewModel.swift; sourceTree = ""; }; - 30571B752C282592009CD5BB /* LSPCache+Data.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LSPCache+Data.swift"; sourceTree = ""; }; 30AB4EBA2BF718A100ED4431 /* DeveloperSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeveloperSettings.swift; sourceTree = ""; }; 30AB4EBC2BF71CA800ED4431 /* DeveloperSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeveloperSettingsView.swift; sourceTree = ""; }; 30AB4EC12BF7253200ED4431 /* KeyValueTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyValueTable.swift; sourceTree = ""; }; @@ -743,7 +749,6 @@ 30B087E72C0D53080063A882 /* LanguageServer+DocumentHighlight.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "LanguageServer+DocumentHighlight.swift"; sourceTree = ""; }; 30B087E82C0D53080063A882 /* LanguageServer+DocumentLink.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "LanguageServer+DocumentLink.swift"; sourceTree = ""; }; 30B087E92C0D53080063A882 /* LanguageServer+DocumentSymbol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "LanguageServer+DocumentSymbol.swift"; sourceTree = ""; }; - 30B087EA2C0D53080063A882 /* LanguageServer+DocumentUtil.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "LanguageServer+DocumentUtil.swift"; sourceTree = ""; }; 30B087EB2C0D53080063A882 /* LanguageServer+FoldingRange.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "LanguageServer+FoldingRange.swift"; sourceTree = ""; }; 30B087EC2C0D53080063A882 /* LanguageServer+Formatting.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "LanguageServer+Formatting.swift"; sourceTree = ""; }; 30B087ED2C0D53080063A882 /* LanguageServer+Hover.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "LanguageServer+Hover.swift"; sourceTree = ""; }; @@ -757,9 +762,8 @@ 30B087F52C0D53080063A882 /* LanguageServer+TypeDefinition.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "LanguageServer+TypeDefinition.swift"; sourceTree = ""; }; 30B087F72C0D53080063A882 /* LanguageServer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LanguageServer.swift; sourceTree = ""; }; 30B087F82C0D53080063A882 /* LSPService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LSPService.swift; sourceTree = ""; }; - 30B087F92C0D53080063A882 /* LSPCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LSPCache.swift; sourceTree = ""; }; 30B087FA2C0D53080063A882 /* LSPUtil.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LSPUtil.swift; sourceTree = ""; }; - 30CB648C2C12680F00CC8A9E /* LSPEventHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LSPEventHandler.swift; sourceTree = ""; }; + 30CB648C2C12680F00CC8A9E /* LSPService+Events.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LSPService+Events.swift"; sourceTree = ""; }; 30E6D0002A6E505200A58B20 /* NavigatorSidebarViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigatorSidebarViewModel.swift; sourceTree = ""; }; 3E0196722A3921AC002648D8 /* codeedit_shell_integration_rc.zsh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = codeedit_shell_integration_rc.zsh; sourceTree = ""; }; 3E0196792A392B45002648D8 /* codeedit_shell_integration.bash */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; path = codeedit_shell_integration.bash; sourceTree = ""; }; @@ -1076,6 +1080,7 @@ 6C6BD70029CD172700235D17 /* ExtensionsListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtensionsListView.swift; sourceTree = ""; }; 6C6BD70329CD17B600235D17 /* ExtensionsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtensionsManager.swift; sourceTree = ""; }; 6C7256D629A3D7D000C2D3E0 /* SplitViewControllerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplitViewControllerView.swift; sourceTree = ""; }; + 6C7D6D452C9092EC00B69EE0 /* BufferingServerConnection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BufferingServerConnection.swift; sourceTree = ""; }; 6C7F37FD2A3EA6FA00217B83 /* View+focusedValue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+focusedValue.swift"; sourceTree = ""; }; 6C81916629B3E80700B75C92 /* ModifierKeysObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModifierKeysObserver.swift; sourceTree = ""; }; 6C82D6B229BFD88700495C54 /* NavigateCommands.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigateCommands.swift; sourceTree = ""; }; @@ -1101,6 +1106,14 @@ 6CC9E4B129B5669900C97388 /* Environment+ActiveEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Environment+ActiveEditor.swift"; sourceTree = ""; }; 6CD035892C3461160091E1F4 /* KeyWindowControllerObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyWindowControllerObserver.swift; sourceTree = ""; }; 6CD03B6929FC773F001BD1D0 /* SettingsInjector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsInjector.swift; sourceTree = ""; }; + 6CD26C6D2C8EA1E600ADBA38 /* LanguageServerFileMap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LanguageServerFileMap.swift; sourceTree = ""; }; + 6CD26C762C8EA83900ADBA38 /* URL+LanguageServer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+LanguageServer.swift"; sourceTree = ""; }; + 6CD26C782C8EA8A500ADBA38 /* LSPCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LSPCache.swift; sourceTree = ""; }; + 6CD26C792C8EA8A500ADBA38 /* LSPCache+Data.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "LSPCache+Data.swift"; sourceTree = ""; }; + 6CD26C7C2C8EA8F400ADBA38 /* LanguageServer+DocumentSync.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "LanguageServer+DocumentSync.swift"; sourceTree = ""; }; + 6CD26C802C8F8A4400ADBA38 /* LanguageIdentifier+CodeLanguage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LanguageIdentifier+CodeLanguage.swift"; sourceTree = ""; }; + 6CD26C862C8F90FD00ADBA38 /* LazyServiceWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LazyServiceWrapper.swift; sourceTree = ""; }; + 6CD26C892C8F91ED00ADBA38 /* LanguageServer+DocumentTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LanguageServer+DocumentTests.swift"; sourceTree = ""; }; 6CDA84AC284C1BA000C1CC3A /* EditorTabBarContextMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditorTabBarContextMenu.swift; sourceTree = ""; }; 6CE21E802C643D8F0031B056 /* CETerminalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CETerminalView.swift; sourceTree = ""; }; 6CE622682A2A174A0013085C /* InspectorTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InspectorTab.swift; sourceTree = ""; }; @@ -1277,6 +1290,7 @@ 58F2EB1E292FB954004A9BDE /* Sparkle in Frameworks */, 6C147C4529A329350089B630 /* OrderedCollections in Frameworks */, 6CE21E872C650D2C0031B056 /* SwiftTerm in Frameworks */, + 6CD3CA552C8B508200D83DCD /* CodeEditSourceEditor in Frameworks */, 6C0617D62BDB4432008C9C42 /* LogStream in Frameworks */, 6CC17B4F2C432AE000834E2C /* CodeEditSourceEditor in Frameworks */, 30CB64912C16CA8100CC8A9E /* LanguageServerProtocol in Frameworks */, @@ -1285,8 +1299,8 @@ 6C85BB442C210EFD00EB5DEF /* SwiftUIIntrospect in Frameworks */, 6CB446402B6DFF3A00539ED0 /* CodeEditSourceEditor in Frameworks */, 2816F594280CF50500DD548B /* CodeEditSymbols in Frameworks */, + 6CD26C852C8F907800ADBA38 /* CodeEditSourceEditor in Frameworks */, 30CB64942C16CA9100CC8A9E /* LanguageClient in Frameworks */, - 6CC17B592C43F53700834E2C /* CodeEditSourceEditor in Frameworks */, 6C6BD6F829CD14D100235D17 /* CodeEditKit in Frameworks */, 6C0824A12C5C0C9700A0751E /* SwiftTerm in Frameworks */, 6C81916B29B41DD300B75C92 /* DequeModule in Frameworks */, @@ -1491,6 +1505,7 @@ 300051662BBD3A5D00A98562 /* ServiceContainer.swift */, 300051692BBD3A8200A98562 /* ServiceType.swift */, 3000516B2BBD3A9500A98562 /* ServiceWrapper.swift */, + 6CD26C862C8F90FD00ADBA38 /* LazyServiceWrapper.swift */, ); path = DependencyInjection; sourceTree = ""; @@ -1531,18 +1546,15 @@ 30B087FB2C0D53080063A882 /* LSP */ = { isa = PBXGroup; children = ( - 30B0881E2C12626B0063A882 /* LanguageServerExtensions */, - 30B087F72C0D53080063A882 /* LanguageServer.swift */, - 30B087F82C0D53080063A882 /* LSPService.swift */, - 30B087F92C0D53080063A882 /* LSPCache.swift */, + 6CD26C822C8F8A5F00ADBA38 /* Extensions */, + 6CD26C732C8EA71F00ADBA38 /* LanguageServer */, + 6CD26C742C8EA79100ADBA38 /* Service */, 30B087FA2C0D53080063A882 /* LSPUtil.swift */, - 30CB648C2C12680F00CC8A9E /* LSPEventHandler.swift */, - 30571B752C282592009CD5BB /* LSPCache+Data.swift */, ); path = LSP; sourceTree = ""; }; - 30B0881E2C12626B0063A882 /* LanguageServerExtensions */ = { + 30B0881E2C12626B0063A882 /* Capabilities */ = { isa = PBXGroup; children = ( 30B087DF2C0D53080063A882 /* LanguageServer+CallHierarchy.swift */, @@ -1551,11 +1563,11 @@ 30B087E32C0D53080063A882 /* LanguageServer+Declaration.swift */, 30B087E42C0D53080063A882 /* LanguageServer+Definition.swift */, 30B087E52C0D53080063A882 /* LanguageServer+Diagnostics.swift */, + 6CD26C7C2C8EA8F400ADBA38 /* LanguageServer+DocumentSync.swift */, 30B087E62C0D53080063A882 /* LanguageServer+DocumentColor.swift */, 30B087E72C0D53080063A882 /* LanguageServer+DocumentHighlight.swift */, 30B087E82C0D53080063A882 /* LanguageServer+DocumentLink.swift */, 30B087E92C0D53080063A882 /* LanguageServer+DocumentSymbol.swift */, - 30B087EA2C0D53080063A882 /* LanguageServer+DocumentUtil.swift */, 30B087EB2C0D53080063A882 /* LanguageServer+FoldingRange.swift */, 30B087EC2C0D53080063A882 /* LanguageServer+Formatting.swift */, 30B087ED2C0D53080063A882 /* LanguageServer+Hover.swift */, @@ -1568,7 +1580,7 @@ 30B087F42C0D53080063A882 /* LanguageServer+SignatureHelp.swift */, 30B087F52C0D53080063A882 /* LanguageServer+TypeDefinition.swift */, ); - path = LanguageServerExtensions; + path = Capabilities; sourceTree = ""; }; 3E0196712A392170002648D8 /* ShellIntegration */ = { @@ -1865,7 +1877,6 @@ isa = PBXGroup; children = ( 5878DAA3291AE76700DD95A3 /* OpenQuicklyViewModel.swift */, - 613899BB2B6E709C00A5CAF6 /* URL+FuzzySearchable.swift */, ); path = ViewModels; sourceTree = ""; @@ -1993,14 +2004,14 @@ isa = PBXGroup; children = ( 283BDCC22972F211002AFF81 /* Acknowledgements */, - 4EE96EC82960562000FFBEA8 /* Documents */, 617DB3DD2C25E11500B58BFE /* ActivityViewer */, - 6141CF392C3DA4180073BC9F /* TerminalEmulator */, - 61FB03A92C3C1FC4001B3671 /* Tasks */, 583E527429361B39001AB554 /* CodeEditUI */, 587B612C2934199800D5CD8F /* CodeFile */, + 4EE96EC82960562000FFBEA8 /* Documents */, + 6CD26C882C8F91B600ADBA38 /* LSP */, 613899BD2B6E70E200A5CAF6 /* Search */, - 6C1F3DA02C18C53C00F6DEF6 /* TerminalEmulator */, + 61FB03A92C3C1FC4001B3671 /* Tasks */, + 6141CF392C3DA4180073BC9F /* TerminalEmulator */, ); path = Features; sourceTree = ""; @@ -2143,7 +2154,6 @@ 587B9E2929301D8F00AC7927 /* GitTime.swift */, 587B9E2A29301D8F00AC7927 /* String+PercentEncoding.swift */, 587B9E2829301D8F00AC7927 /* String+QueryParameters.swift */, - 587B9E2729301D8F00AC7927 /* URL+URLParameters.swift */, ); path = Utils; sourceTree = ""; @@ -2431,6 +2441,7 @@ 58D01C8B293167DC00C5B6B4 /* String */, 5831E3CB2933E89A00D5A6D2 /* SwiftTerm */, 6CBD1BC42978DE3E006639D5 /* Text */, + 6CD26C752C8EA80000ADBA38 /* URL */, 5831E3CA2933E86F00D5A6D2 /* View */, ); path = Extensions; @@ -2597,6 +2608,7 @@ 6141CF392C3DA4180073BC9F /* TerminalEmulator */ = { isa = PBXGroup; children = ( + 6C1F3DA12C18C55800F6DEF6 /* ShellIntegrationTests.swift */, 61FB03AA2C3C1FD5001B3671 /* Shell */, ); name = TerminalEmulator; @@ -2819,14 +2831,6 @@ path = FindNavigatorResultList; sourceTree = ""; }; - 6C1F3DA02C18C53C00F6DEF6 /* TerminalEmulator */ = { - isa = PBXGroup; - children = ( - 6C1F3DA12C18C55800F6DEF6 /* ShellIntegrationTests.swift */, - ); - path = TerminalEmulator; - sourceTree = ""; - }; 6C2384302C796EBD003FBDD4 /* ChangedFile */ = { isa = PBXGroup; children = ( @@ -2987,6 +2991,54 @@ path = CodeFileDocument; sourceTree = ""; }; + 6CD26C732C8EA71F00ADBA38 /* LanguageServer */ = { + isa = PBXGroup; + children = ( + 30B087F72C0D53080063A882 /* LanguageServer.swift */, + 6CD26C6D2C8EA1E600ADBA38 /* LanguageServerFileMap.swift */, + 6CD26C782C8EA8A500ADBA38 /* LSPCache.swift */, + 6CD26C792C8EA8A500ADBA38 /* LSPCache+Data.swift */, + 30B0881E2C12626B0063A882 /* Capabilities */, + ); + path = LanguageServer; + sourceTree = ""; + }; + 6CD26C742C8EA79100ADBA38 /* Service */ = { + isa = PBXGroup; + children = ( + 30B087F82C0D53080063A882 /* LSPService.swift */, + 30CB648C2C12680F00CC8A9E /* LSPService+Events.swift */, + ); + path = Service; + sourceTree = ""; + }; + 6CD26C752C8EA80000ADBA38 /* URL */ = { + isa = PBXGroup; + children = ( + 613899BB2B6E709C00A5CAF6 /* URL+FuzzySearchable.swift */, + 6CD26C762C8EA83900ADBA38 /* URL+LanguageServer.swift */, + 587B9E2729301D8F00AC7927 /* URL+URLParameters.swift */, + ); + path = URL; + sourceTree = ""; + }; + 6CD26C822C8F8A5F00ADBA38 /* Extensions */ = { + isa = PBXGroup; + children = ( + 6CD26C802C8F8A4400ADBA38 /* LanguageIdentifier+CodeLanguage.swift */, + ); + path = Extensions; + sourceTree = ""; + }; + 6CD26C882C8F91B600ADBA38 /* LSP */ = { + isa = PBXGroup; + children = ( + 6CD26C892C8F91ED00ADBA38 /* LanguageServer+DocumentTests.swift */, + 6C7D6D452C9092EC00B69EE0 /* BufferingServerConnection.swift */, + ); + path = LSP; + sourceTree = ""; + }; 6CFBA54E2C4E182100E3A914 /* Other Tests */ = { isa = PBXGroup; children = ( @@ -3612,10 +3664,11 @@ 6C85BB3F2C2105ED00EB5DEF /* CodeEditKit */, 6C85BB432C210EFD00EB5DEF /* SwiftUIIntrospect */, 6CC17B4E2C432AE000834E2C /* CodeEditSourceEditor */, - 6CC17B582C43F53700834E2C /* CodeEditSourceEditor */, 6C0824A02C5C0C9700A0751E /* SwiftTerm */, 6CE21E862C650D2C0031B056 /* SwiftTerm */, 6C4E37FB2C73E00700AEE7B5 /* SwiftTerm */, + 6CD3CA542C8B508200D83DCD /* CodeEditSourceEditor */, + 6CD26C842C8F907800ADBA38 /* CodeEditSourceEditor */, ); productName = CodeEdit; productReference = B658FB2C27DA9E0F00EA4DBD /* CodeEdit.app */; @@ -3711,8 +3764,8 @@ 6C85BB422C210EFD00EB5DEF /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */, 303E88452C276FD100EEA8D9 /* XCRemoteSwiftPackageReference "LanguageClient" */, 303E88462C276FD600EEA8D9 /* XCRemoteSwiftPackageReference "LanguageServerProtocol" */, - 6CC17B572C43F53700834E2C /* XCRemoteSwiftPackageReference "CodeEditSourceEditor" */, 6C4E37FA2C73E00700AEE7B5 /* XCRemoteSwiftPackageReference "SwiftTerm" */, + 6CD26C832C8F907800ADBA38 /* XCRemoteSwiftPackageReference "CodeEditSourceEditor" */, ); productRefGroup = B658FB2D27DA9E0F00EA4DBD /* Products */; projectDirPath = ""; @@ -3859,11 +3912,13 @@ 3000516C2BBD3A9500A98562 /* ServiceWrapper.swift in Sources */, 587B9DA029300ABD00AC7927 /* PanelDivider.swift in Sources */, 201169E52837B40300F92B46 /* SourceControlNavigatorRepositoryView.swift in Sources */, + 6CD26C6E2C8EA1E600ADBA38 /* LanguageServerFileMap.swift in Sources */, 587B9E6A29301D8F00AC7927 /* GitLabPermissions.swift in Sources */, B6EA1FF529DA380E001BF195 /* TextEditingSettingsView.swift in Sources */, D7DC4B76298FFBE900D6C83D /* ProjectNavigatorViewController+OutlineTableViewCellDelegate.swift in Sources */, 587B9E9229301D8F00AC7927 /* BitBucketAccount.swift in Sources */, DE513F52281B672D002260B9 /* EditorTabBarAccessory.swift in Sources */, + 6CD26C7A2C8EA8A500ADBA38 /* LSPCache.swift in Sources */, 618725A42C29F00400987354 /* WorkspaceMenuItemView.swift in Sources */, 2813F93927ECC4C300E305E4 /* NavigatorAreaView.swift in Sources */, B664C3B02B965F6C00816B4E /* NavigationSettings.swift in Sources */, @@ -3911,7 +3966,6 @@ 66AF6CE42BF17F6800D83C9D /* StatusBarFileInfoView.swift in Sources */, 587B9E5E29301D8F00AC7927 /* GitLabCommitRouter.swift in Sources */, 58F2EB0D292FB2B0004A9BDE /* ThemeSettings.swift in Sources */, - 30571B762C282592009CD5BB /* LSPCache+Data.swift in Sources */, 587B9D9F29300ABD00AC7927 /* SegmentedControl.swift in Sources */, 6C7256D729A3D7D000C2D3E0 /* SplitViewControllerView.swift in Sources */, B6EA1FE529DA33DB001BF195 /* ThemeModel.swift in Sources */, @@ -3963,7 +4017,7 @@ 6C2C156129B4F52F00EA60A5 /* SplitViewModifiers.swift in Sources */, 61A53A812B4449F00093BF8A /* WorkspaceDocument+Index.swift in Sources */, 66AF6CE22BF17CC300D83C9D /* StatusBarViewModel.swift in Sources */, - 30CB648D2C12680F00CC8A9E /* LSPEventHandler.swift in Sources */, + 30CB648D2C12680F00CC8A9E /* LSPService+Events.swift in Sources */, 201169DD2837B3AC00F92B46 /* SourceControlNavigatorToolbarBottom.swift in Sources */, 587B9E8B29301D8F00AC7927 /* GitHubAccount+deleteReference.swift in Sources */, 6CD0358A2C3461160091E1F4 /* KeyWindowControllerObserver.swift in Sources */, @@ -4067,6 +4121,7 @@ 581550CF29FBD30400684881 /* StandardTableViewCell.swift in Sources */, B62AEDB82A1FE2DC009A9F52 /* UtilityAreaOutputView.swift in Sources */, B67DB0FC2AFDF71F002DC647 /* IconToggleStyle.swift in Sources */, + 6CD26C812C8F8A4400ADBA38 /* LanguageIdentifier+CodeLanguage.swift in Sources */, 5994B6DA2BD6B408006A4C5F /* Editor+TabSwitch.swift in Sources */, 587B9E5C29301D8F00AC7927 /* Parameters.swift in Sources */, 61538B932B11201900A88846 /* String+Character.swift in Sources */, @@ -4102,6 +4157,7 @@ B66A4E4529C8E86D004573B4 /* CommandsFixes.swift in Sources */, B6966A2A2C2F687A00259C2D /* SourceControlFetchView.swift in Sources */, 5882252F292C280D00E83CDE /* UtilityAreaClearButton.swift in Sources */, + 6CD26C7D2C8EA8F400ADBA38 /* LanguageServer+DocumentSync.swift in Sources */, 6CE622692A2A174A0013085C /* InspectorTab.swift in Sources */, 58F2EB04292FB2B0004A9BDE /* SourceControlSettings.swift in Sources */, 618725AB2C29F2C000987354 /* TaskDropDownView.swift in Sources */, @@ -4167,7 +4223,6 @@ 610C0FDA2B44438F00A01CA7 /* WorkspaceDocument+FindAndReplace.swift in Sources */, B67431CC2C3E45F30047FCA6 /* SourceControlSwitchView.swift in Sources */, 5882252C292C280D00E83CDE /* UtilityAreaView.swift in Sources */, - 30B088072C0D53080063A882 /* LanguageServer+DocumentUtil.swift in Sources */, 2847019E27FDDF7600F87B6B /* ProjectNavigatorOutlineView.swift in Sources */, B607184C2B17E037009CDAB4 /* SourceControlStashView.swift in Sources */, 6C14CEB32877A68F001468FE /* FindNavigatorMatchListCell.swift in Sources */, @@ -4199,12 +4254,14 @@ B697937A29FF5668002027EC /* AccountsSettingsAccountLink.swift in Sources */, 5B698A0D2B26327800DE9392 /* SearchSettings.swift in Sources */, B685DE7929CC9CCD002860C8 /* StatusBarIcon.swift in Sources */, + 6CD26C872C8F90FD00ADBA38 /* LazyServiceWrapper.swift in Sources */, 587B9DA629300ABD00AC7927 /* ToolbarBranchPicker.swift in Sources */, 6C6BD6F629CD145F00235D17 /* ExtensionInfo.swift in Sources */, 04BA7C202AE2D92B00584E1C /* GitClient+Status.swift in Sources */, 58F2EB05292FB2B0004A9BDE /* Settings.swift in Sources */, 6CBD1BC62978DE53006639D5 /* Font+Caption3.swift in Sources */, 30E6D0012A6E505200A58B20 /* NavigatorSidebarViewModel.swift in Sources */, + 6CD26C7B2C8EA8A500ADBA38 /* LSPCache+Data.swift in Sources */, B6E41C9429DEAE260088F9F4 /* SourceControlAccount.swift in Sources */, 2806E9022979588B000040F4 /* Contributor.swift in Sources */, 58D01C98293167DC00C5B6B4 /* String+RemoveOccurrences.swift in Sources */, @@ -4267,6 +4324,7 @@ 6C147C4129A328BF0089B630 /* EditorLayout.swift in Sources */, B6D7EA592971078500301FAC /* InspectorSection.swift in Sources */, B6AB09A32AAABFEC0003A3A6 /* EditorTabBarLeadingAccessories.swift in Sources */, + 6CD26C772C8EA83900ADBA38 /* URL+LanguageServer.swift in Sources */, B69BFDC72B0686910050D9A6 /* GitClient+Initiate.swift in Sources */, 58F2EAEF292FB2B0004A9BDE /* ThemeSettingsView.swift in Sources */, 85745D632A38F8D900089AAB /* String+HighlightOccurrences.swift in Sources */, @@ -4281,7 +4339,6 @@ 5878DAA5291AE76700DD95A3 /* OpenQuicklyView.swift in Sources */, B6966A302C33282200259C2D /* RemoteBranchPicker.swift in Sources */, 201169D72837B2E300F92B46 /* SourceControlNavigatorView.swift in Sources */, - 30B088162C0D53080063A882 /* LSPCache.swift in Sources */, B6F0517929D9E3C900D72287 /* SourceControlGitView.swift in Sources */, 587B9E8329301D8F00AC7927 /* GitHubPullRequest.swift in Sources */, 6C85F7562C3CA638008E9836 /* EditorHistoryMenus.swift in Sources */, @@ -4370,6 +4427,7 @@ 61FB03B02C3C76AF001B3671 /* TaskManagerTests.swift in Sources */, 61FB03AC2C3C1FDF001B3671 /* ShellTests.swift in Sources */, 583E528C29361B39001AB554 /* CodeEditUITests.swift in Sources */, + 6C7D6D462C9092EC00B69EE0 /* BufferingServerConnection.swift in Sources */, 613053652B23A49300D767E3 /* TemporaryFile.swift in Sources */, 617DB3DF2C25E13800B58BFE /* TaskNotificationHandlerTests.swift in Sources */, 775566502C27FD1B001E7A4D /* CodeFileDocument+UTTypeTests.swift in Sources */, @@ -4378,6 +4436,7 @@ 587B61012934170A00D5CD8F /* UnitTests_Extensions.swift in Sources */, 6C1F3DA22C18C55800F6DEF6 /* ShellIntegrationTests.swift in Sources */, 283BDCC52972F236002AFF81 /* AcknowledgementsTests.swift in Sources */, + 6CD26C8A2C8F91ED00ADBA38 /* LanguageServer+DocumentTests.swift in Sources */, 4EE96ECB2960565E00FFBEA8 /* DocumentsUnitTests.swift in Sources */, 4EE96ECE296059E000FFBEA8 /* NSHapticFeedbackPerformerMock.swift in Sources */, 61FB03AE2C3C2493001B3671 /* CEActiveTaskTests.swift in Sources */, @@ -5588,7 +5647,7 @@ minimumVersion = 1.2.0; }; }; - 6CC17B572C43F53700834E2C /* XCRemoteSwiftPackageReference "CodeEditSourceEditor" */ = { + 6CD26C832C8F907800ADBA38 /* XCRemoteSwiftPackageReference "CodeEditSourceEditor" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/CodeEditApp/CodeEditSourceEditor"; requirement = { @@ -5685,9 +5744,13 @@ isa = XCSwiftPackageProductDependency; productName = CodeEditSourceEditor; }; - 6CC17B582C43F53700834E2C /* CodeEditSourceEditor */ = { + 6CD26C842C8F907800ADBA38 /* CodeEditSourceEditor */ = { + isa = XCSwiftPackageProductDependency; + package = 6CD26C832C8F907800ADBA38 /* XCRemoteSwiftPackageReference "CodeEditSourceEditor" */; + productName = CodeEditSourceEditor; + }; + 6CD3CA542C8B508200D83DCD /* CodeEditSourceEditor */ = { isa = XCSwiftPackageProductDependency; - package = 6CC17B572C43F53700834E2C /* XCRemoteSwiftPackageReference "CodeEditSourceEditor" */; productName = CodeEditSourceEditor; }; 6CE21E862C650D2C0031B056 /* SwiftTerm */ = { diff --git a/CodeEdit/AppDelegate.swift b/CodeEdit/AppDelegate.swift index e86b55a79..a3ce021f3 100644 --- a/CodeEdit/AppDelegate.swift +++ b/CodeEdit/AppDelegate.swift @@ -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 { diff --git a/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFileManager+FileManagement.swift b/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFileManager+FileManagement.swift index cc536520b..e3469687f 100644 --- a/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFileManager+FileManagement.swift +++ b/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFileManager+FileManagement.swift @@ -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 diff --git a/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFileManager.swift b/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFileManager.swift index 07402f957..4a3a97b83 100644 --- a/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFileManager.swift +++ b/CodeEdit/Features/CEWorkspace/Models/CEWorkspaceFileManager.swift @@ -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 } diff --git a/CodeEdit/Features/Documents/CodeFileDocument/CodeFileDocument.swift b/CodeEdit/Features/Documents/CodeFileDocument/CodeFileDocument.swift index afcfda12d..b5b70f4d6 100644 --- a/CodeEdit/Features/Documents/CodeFileDocument/CodeFileDocument.swift +++ b/CodeEdit/Features/Documents/CodeFileDocument/CodeFileDocument.swift @@ -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 @@ -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? @@ -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 + } } diff --git a/CodeEdit/Features/Documents/Controllers/CodeEditDocumentController.swift b/CodeEdit/Features/Documents/Controllers/CodeEditDocumentController.swift index a90db1e38..6e976d049 100644 --- a/CodeEdit/Features/Documents/Controllers/CodeEditDocumentController.swift +++ b/CodeEdit/Features/Documents/Controllers/CodeEditDocumentController.swift @@ -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?) { @@ -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: @@ -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 { diff --git a/CodeEdit/Features/Documents/WorkspaceDocument/WorkspaceDocument.swift b/CodeEdit/Features/Documents/WorkspaceDocument/WorkspaceDocument.swift index 83eb1a045..6af153fae 100644 --- a/CodeEdit/Features/Documents/WorkspaceDocument/WorkspaceDocument.swift +++ b/CodeEdit/Features/Documents/WorkspaceDocument/WorkspaceDocument.swift @@ -13,7 +13,6 @@ import LanguageServerProtocol @objc(WorkspaceDocument) final class WorkspaceDocument: NSDocument, ObservableObject, NSToolbarDelegate { - @Published var sortFoldersOnTop: Bool = true private var workspaceState: [String: Any] { diff --git a/CodeEdit/Features/Editor/Models/Editor.swift b/CodeEdit/Features/Editor/Models/Editor.swift index e8a4e5afb..f22648a6c 100644 --- a/CodeEdit/Features/Editor/Models/Editor.swift +++ b/CodeEdit/Features/Editor/Models/Editor.swift @@ -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 ?? "" ) diff --git a/CodeEdit/Features/Editor/Views/CodeFileView.swift b/CodeEdit/Features/Editor/Views/CodeFileView.swift index 627d96074..e6cccee5c 100644 --- a/CodeEdit/Features/Editor/Views/CodeFileView.swift +++ b/CodeEdit/Features/Editor/Views/CodeFileView.swift @@ -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, @@ -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 diff --git a/CodeEdit/Features/Editor/Views/EditorAreaView.swift b/CodeEdit/Features/Editor/Views/EditorAreaView.swift index a08a0a5d6..bc88ad20a 100644 --- a/CodeEdit/Features/Editor/Views/EditorAreaView.swift +++ b/CodeEdit/Features/Editor/Views/EditorAreaView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CodeEditTextView struct EditorAreaView: View { @AppSettings(\.general.showEditorPathBar) @@ -25,6 +26,12 @@ struct EditorAreaView: View { @State var codeFile: CodeFileDocument? + init(editor: Editor, focus: FocusState.Binding) { + self.editor = editor + self._focus = focus + self.codeFile = editor.selectedTab?.file.fileDocument + } + var body: some View { var shouldShowTabBar: Bool { return navigationStyle == .openInTabs @@ -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 { @@ -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] diff --git a/CodeEdit/Features/LSP/Extensions/LanguageIdentifier+CodeLanguage.swift b/CodeEdit/Features/LSP/Extensions/LanguageIdentifier+CodeLanguage.swift new file mode 100644 index 000000000..b9171f74d --- /dev/null +++ b/CodeEdit/Features/LSP/Extensions/LanguageIdentifier+CodeLanguage.swift @@ -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 + } + } +} diff --git a/CodeEdit/Features/LSP/LSPEventHandler.swift b/CodeEdit/Features/LSP/LSPEventHandler.swift deleted file mode 100644 index ae6ee28b4..000000000 --- a/CodeEdit/Features/LSP/LSPEventHandler.swift +++ /dev/null @@ -1,93 +0,0 @@ -// -// LSPEventHandler.swift -// CodeEdit -// -// Created by Abe Malla on 6/1/24. -// - -import LanguageClient -import LanguageServerProtocol - -extension LSPService { - func startListeningToEvents(for languageId: LanguageIdentifier) { - guard let languageClient = languageClients[languageId] else { - logger.error("Language client not found for \(languageId.rawValue)") - return - } - - // Create a new Task to listen to the events - let task = Task { - for await event in languageClient.lspInstance.eventSequence { - handleEvent(event, for: languageId) - } - } - eventListeningTasks[languageId] = task - } - - func stopListeningToEvents(for languageId: LanguageIdentifier) { - if let task = eventListeningTasks[languageId] { - task.cancel() - eventListeningTasks.removeValue(forKey: languageId) - } - } - - private func handleEvent(_ event: ServerEvent, for languageId: LanguageIdentifier) { - switch event { - case let .request(id, request): - print("Request ID: \(id) for \(languageId.rawValue)") - handleRequest(request) - case let .notification(notification): - handleNotification(notification) - case let .error(error): - print("Error from EventStream for \(languageId.rawValue): \(error)") - } - } - - // swiftlint:disable:next cyclomatic_complexity - private func handleRequest(_ request: ServerRequest) { - switch request { - case let .workspaceConfiguration(params, _): - print("workspaceConfiguration: \(params)") - case let .workspaceFolders(handler): - print("workspaceFolders: \(String(describing: handler))") - case let .workspaceApplyEdit(params, _): - print("workspaceApplyEdit: \(params)") - case let .clientRegisterCapability(params, _): - print("clientRegisterCapability: \(params)") - case let .clientUnregisterCapability(params, _): - print("clientUnregisterCapability: \(params)") - case let .workspaceCodeLensRefresh(handler): - print("workspaceCodeLensRefresh: \(String(describing: handler))") - case let .workspaceSemanticTokenRefresh(handler): - print("workspaceSemanticTokenRefresh: \(String(describing: handler))") - case let .windowShowMessageRequest(params, _): - print("windowShowMessageRequest: \(params)") - case let .windowShowDocument(params, _): - print("windowShowDocument: \(params)") - case let .windowWorkDoneProgressCreate(params, _): - print("windowWorkDoneProgressCreate: \(params)") - - default: - print() - } - } - - private func handleNotification(_ notification: ServerNotification) { - switch notification { - case let .windowLogMessage(params): - print("windowLogMessage \(params.type)\n```\n\(params.message)\n```\n") - case let .windowShowMessage(params): - print("windowShowMessage \(params.type)\n```\n\(params.message)\n```\n") - case let .textDocumentPublishDiagnostics(params): - print("textDocumentPublishDiagnostics: \(params)") - case let .telemetryEvent(params): - print("telemetryEvent: \(params)") - case let .protocolCancelRequest(params): - print("protocolCancelRequest: \(params)") - case let .protocolProgress(params): - print("protocolProgress: \(params)") - case let .protocolLogTrace(params): - print("protocolLogTrace: \(params)") - } - } -} diff --git a/CodeEdit/Features/LSP/LSPService.swift b/CodeEdit/Features/LSP/LSPService.swift deleted file mode 100644 index a4846b560..000000000 --- a/CodeEdit/Features/LSP/LSPService.swift +++ /dev/null @@ -1,212 +0,0 @@ -// -// LSPService.swift -// CodeEdit -// -// Created by Abe Malla on 2/7/24. -// - -import os.log -import JSONRPC -import Foundation -import LanguageClient -import LanguageServerProtocol - -/// `LSPService` is a service class responsible for managing the lifecycle and event handling -/// of Language Server Protocol (LSP) clients within the CodeEdit application. It handles the initialization, -/// communication, and termination of language servers, ensuring that code assistance features -/// such as code completion, diagnostics, and more are available for various programming languages. -/// -/// This class uses Swift's concurrency model to manage background tasks and event streams -/// efficiently. Each language server runs in its own asynchronous task, listening for events and -/// handling them as they occur. The `LSPService` class also provides functionality to start -/// and stop individual language servers, as well as to stop all running servers. -/// -/// ## Example Usage -/// ```swift -/// @Service var lspService -/// -/// try await lspService.startServer( -/// for: .python, -/// projectURL: projectURL, -/// workspaceFolders: workspaceFolders -/// ) -/// try await lspService.stopServer(for: .python) -/// ``` -final class LSPService: ObservableObject { - internal let logger: Logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "", category: "LSPService") - - /// Holds the active language clients - internal var languageClients: [LanguageIdentifier: LanguageServer] = [:] - /// Holds the language server configurations for all the installed language servers - internal var languageConfigs: [LanguageIdentifier: LanguageServerBinary] = [:] - /// Holds all the event listeners for each active language client - internal var eventListeningTasks: [LanguageIdentifier: Task] = [:] - - @AppSettings(\.developerSettings.lspBinaries) - internal var lspBinaries - - init() { - // Load the LSP binaries from the developer menu - for binary in lspBinaries { - if let language = LanguageIdentifier(rawValue: binary.key) { - self.languageConfigs[language] = LanguageServerBinary( - execPath: binary.value, - args: [], - env: ProcessInfo.processInfo.environment - ) - } - } - } - - /// Gets the language server for the specified language - func server(for languageId: LanguageIdentifier) -> InitializingServer? { - return languageClients[languageId]?.lspInstance - } - - /// Gets the language client for the specified language - func languageClient(for languageId: LanguageIdentifier) -> LanguageServer? { - return languageClients[languageId] - } - - /// Given a language, will attempt to start the language server - func startServer( - for languageId: LanguageIdentifier, - projectURL: URL, - workspaceFolders: [WorkspaceFolder]? - ) async throws { - guard let serverBinary = languageConfigs[languageId] else { - logger.error("Couldn't find language sever binary for \(languageId.rawValue)") - throw LSPError.binaryNotFound - } - - let server = try LanguageServer.createServer( - for: languageId, - with: serverBinary, - rootPath: projectURL, - workspaceFolders: workspaceFolders - ) - languageClients[languageId] = server - - logger.info("Initializing \(languageId.rawValue) language server") - try await server.initialize() - logger.info("Successfully initialized \(languageId.rawValue) language server") - - self.startListeningToEvents(for: languageId) - } - - /// Notify the proper language server that we opened a document. - func documentWasOpened(for languageId: LanguageIdentifier, file fileURL: URL) async throws -> Bool { - // TODO: GET FILE TYPE FROM DOCUMENT, USING NEW FILE SOLUTION - guard var languageClient = self.languageClient(for: .python) else { - logger.error("Failed to get \(languageId.rawValue) client") - throw ServerManagerError.languageClientNotFound - } - return await languageClient.addDocument(fileURL) - } - - /// Notify the proper language server that we closed a document so we can stop tracking the file. - func documentWasClosed(for languageId: LanguageIdentifier, file fileURL: URL) async throws -> Bool { - // TODO: GET FILE TYPE FROM DOCUMENT, USING NEW FILE SOLUTION - guard var languageClient = self.languageClient(for: .python) else { - logger.error("Failed to get \(languageId.rawValue) client") - throw ServerManagerError.languageClientNotFound - } - return await languageClient.closeDocument(fileURL.absoluteString) - } - - /// NOTE: This function is intended to be removed when the frontend is being developed. - /// For now this is just for reference of a working example. - func testCompletion() async throws { - do { - guard var languageClient = self.languageClient(for: .python) else { - print("Failed to get client") - throw ServerManagerError.languageClientNotFound - } - - let testFilePathStr = "" - let testFileURL = URL(fileURLWithPath: testFilePathStr) - - // Tell server we opened a document - _ = await languageClient.addDocument(testFileURL) - - // Completion example - let textPosition = Position(line: 32, character: 18) // Lines and characters start at 0 - let completions = try await languageClient.requestCompletion( - document: testFileURL.absoluteString, - position: textPosition - ) - switch completions { - case .optionA(let completionItems): - // Handle the case where completions is an array of CompletionItem - print("\n*******\nCompletion Items:\n*******\n") - for item in completionItems { - let textEdits = LSPCompletionItemsUtil.getCompletionItemEdits( - startPosition: textPosition, - item: item - ) - for edit in textEdits { - print(edit) - } - } - - case .optionB(let completionList): - // Handle the case where completions is a CompletionList - print("\n*******\nCompletion Items:\n*******\n") - for item in completionList.items { - let textEdits = LSPCompletionItemsUtil.getCompletionItemEdits( - startPosition: textPosition, - item: item - ) - for edit in textEdits { - print(edit) - } - } - - print(completionList.items[0]) - - case .none: - print("No completions found") - } - - // Close the document - _ = await languageClient.closeDocument(testFilePathStr) - } catch { - print(error) - } - } - - /// Attempts to stop a running language server. Throws an error if the server is not found - /// or if the language server throws an error while trying to shutdown. - func stopServer(for languageId: LanguageIdentifier) async throws { - guard let server = self.server(for: languageId) else { - logger.error("Server not found for language \(languageId.rawValue) during stop operation") - throw ServerManagerError.serverNotFound - } - do { - try await server.shutdownAndExit() - } catch { - logger.error("Failed to stop server for language \(languageId.rawValue): \(error.localizedDescription)") - throw error - } - languageClients.removeValue(forKey: languageId) - logger.info("Server stopped for language \(languageId.rawValue)") - - stopListeningToEvents(for: languageId) - } - - /// Goes through all active language servers and attempts to shut them down. - func stopAllServers() async throws { - for languageId in languageClients.keys { - try await stopServer(for: languageId) - } - } -} - -// MARK: - Errors - -enum ServerManagerError: Error { - case serverNotFound - case serverStartFailed - case serverStopFailed - case languageClientNotFound -} diff --git a/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+CallHierarchy.swift b/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+CallHierarchy.swift new file mode 100644 index 000000000..3cf99ba12 --- /dev/null +++ b/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+CallHierarchy.swift @@ -0,0 +1,57 @@ +// +// LanguageServer+CallHierarchy.swift +// CodeEdit +// +// Created by Abe Malla on 2/7/24. +// + +import Foundation +import LanguageServerProtocol + +extension LanguageServer { + func requestPrepareCallHierarchy( + for documentURI: String, position: Position + ) async throws -> CallHierarchyPrepareResponse { + do { + let prepareParams = CallHierarchyPrepareParams( + textDocument: TextDocumentIdentifier(uri: documentURI), + position: position, + workDoneToken: nil + ) + return try await lspInstance.prepareCallHierarchy(prepareParams) + } catch { + logger.warning("requestPrepareCallHierarchy: Error \(error)") + throw error + } + } + + func requestCallHierarchyIncomingCalls( + _ callHierarchyItem: CallHierarchyItem + ) async throws -> CallHierarchyIncomingCallsResponse { + do { + let incomingParams = CallHierarchyIncomingCallsParams( + item: callHierarchyItem, + workDoneToken: nil + ) + return try await lspInstance.callHierarchyIncomingCalls(incomingParams) + } catch { + logger.warning("requestCallHierarchyIncomingCalls: Error \(error)") + throw error + } + } + + func requestCallHierarchyOutgoingCalls( + _ callHierarchyItem: CallHierarchyItem + ) async throws -> CallHierarchyOutgoingCallsResponse { + do { + let outgoingParams = CallHierarchyOutgoingCallsParams( + item: callHierarchyItem, + workDoneToken: nil + ) + return try await lspInstance.callHierarchyOutgoingCalls(outgoingParams) + } catch { + logger.warning("requestCallHierarchyOutgoingCalls: Error \(error)") + throw error + } + } +} diff --git a/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+ColorPresentation.swift b/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+ColorPresentation.swift new file mode 100644 index 000000000..1b054ea9a --- /dev/null +++ b/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+ColorPresentation.swift @@ -0,0 +1,31 @@ +// +// LanguageServer+ColorPresentation.swift +// CodeEdit +// +// Created by Abe Malla on 2/7/24. +// + +import Foundation +import LanguageServerProtocol + +extension LanguageServer { + func requestColorPresentation( + for documentURI: String, + color: Color, + range: LSPRange + ) async throws -> ColorPresentationResponse { + do { + let params = ColorPresentationParams( + workDoneToken: nil, + partialResultToken: nil, + textDocument: TextDocumentIdentifier(uri: documentURI), + color: color, + range: range + ) + return try await lspInstance.colorPresentation(params) + } catch { + logger.warning("requestColorPresentation: Error \(error)") + throw error + } + } +} diff --git a/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+Completion.swift b/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+Completion.swift new file mode 100644 index 000000000..ab9cfdaea --- /dev/null +++ b/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+Completion.swift @@ -0,0 +1,37 @@ +// +// LanguageServer+Completion.swift +// CodeEdit +// +// Created by Abe Malla on 2/7/24. +// + +import Foundation +import LanguageServerProtocol + +extension LanguageServer { + func requestCompletion(for documentURI: String, position: Position) async throws -> CompletionResponse { + do { + let cacheKey = CacheKey( + uri: documentURI, + requestType: "completion", + extraData: position + ) + if let cachedResponse: CompletionResponse = lspCache.get(key: cacheKey, as: CompletionResponse.self) { + return cachedResponse + } + let completionParams = CompletionParams( + uri: documentURI, + position: position, + triggerKind: .invoked, + triggerCharacter: nil + ) + let response = try await lspInstance.completion(completionParams) + + lspCache.set(key: cacheKey, value: response) + return response + } catch { + logger.warning("requestCompletion: Error \(error)") + throw error + } + } +} diff --git a/CodeEdit/Features/LSP/LanguageServerExtensions/LanguageServer+Declaration.swift b/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+Declaration.swift similarity index 68% rename from CodeEdit/Features/LSP/LanguageServerExtensions/LanguageServer+Declaration.swift rename to CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+Declaration.swift index 28aec4a60..0be433492 100644 --- a/CodeEdit/Features/LSP/LanguageServerExtensions/LanguageServer+Declaration.swift +++ b/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+Declaration.swift @@ -9,7 +9,7 @@ import Foundation import LanguageServerProtocol extension LanguageServer { - func requestGoToDeclaration(document documentURI: String, _ position: Position) async -> DeclarationResponse { + func requestGoToDeclaration(for documentURI: String, position: Position) async throws -> DeclarationResponse { do { let params = TextDocumentPositionParams( textDocument: TextDocumentIdentifier(uri: documentURI), @@ -17,9 +17,8 @@ extension LanguageServer { ) return try await lspInstance.declaration(params) } catch { - print("requestGoToDeclaration Error \(error)") + logger.warning("requestGoToDeclaration: Error \(error)") + throw error } - - return nil } } diff --git a/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+Definition.swift b/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+Definition.swift new file mode 100644 index 000000000..8882f73a5 --- /dev/null +++ b/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+Definition.swift @@ -0,0 +1,37 @@ +// +// LanguageServer+Definition.swift +// CodeEdit +// +// Created by Abe Malla on 2/7/24. +// + +import Foundation +import LanguageServerProtocol + +extension LanguageServer { + func requestGoToDefinition(for documentURI: String, position: Position) async throws -> DefinitionResponse { + do { + let cacheKey = CacheKey( + uri: documentURI, + requestType: "goToDefinition", + extraData: NoExtraData() + ) + if let cachedResponse: DefinitionResponse = lspCache.get(key: cacheKey, as: DefinitionResponse.self) { + return cachedResponse + } + + let textDocumentIdentifier = TextDocumentIdentifier(uri: documentURI) + let textDocumentPositionParams = TextDocumentPositionParams( + textDocument: textDocumentIdentifier, + position: position + ) + let response = try await lspInstance.definition(textDocumentPositionParams) + + lspCache.set(key: cacheKey, value: response) + return response + } catch { + logger.warning("requestGoToDefinition: Error \(error)") + throw error + } + } +} diff --git a/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+Diagnostics.swift b/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+Diagnostics.swift new file mode 100644 index 000000000..b2ea13536 --- /dev/null +++ b/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+Diagnostics.swift @@ -0,0 +1,37 @@ +// +// LanguageServer+Diagnostics.swift +// CodeEdit +// +// Created by Abe Malla on 2/7/24. +// + +import Foundation +import LanguageServerProtocol + +extension LanguageServer { + func requestPullDiagnostics(document documentURI: String) async throws -> DocumentDiagnosticReport { + do { + let cacheKey = CacheKey( + uri: documentURI, + requestType: "diagnostics", + extraData: NoExtraData() + ) + if let cachedResponse: DocumentDiagnosticReport = lspCache.get( + key: cacheKey, as: DocumentDiagnosticReport.self + ) { + return cachedResponse + } + + let response = try await lspInstance.diagnostics( + DocumentDiagnosticParams( + textDocument: TextDocumentIdentifier(uri: documentURI) + ) + ) + lspCache.set(key: cacheKey, value: response) + return response + } catch { + logger.warning("requestPullDiagnostics: Error \(error)") + throw error + } + } +} diff --git a/CodeEdit/Features/LSP/LanguageServerExtensions/LanguageServer+DocumentColor.swift b/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+DocumentColor.swift similarity index 65% rename from CodeEdit/Features/LSP/LanguageServerExtensions/LanguageServer+DocumentColor.swift rename to CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+DocumentColor.swift index 711bc6ab7..8933067ef 100644 --- a/CodeEdit/Features/LSP/LanguageServerExtensions/LanguageServer+DocumentColor.swift +++ b/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+DocumentColor.swift @@ -14,17 +14,17 @@ extension LanguageServer { /// Clients can use the result to decorate color references in an editor. For example: /// 1. Color boxes showing the actual color next to the reference /// 2. Show a color picker when a color reference is edited - func requestDocumentColor(document documentURI: String) async -> DocumentColorResponse { + func requestColor(for documentURI: String) async throws -> DocumentColorResponse { + let params = DocumentColorParams( + textDocument: TextDocumentIdentifier(uri: documentURI), + workDoneToken: nil, + partialResultToken: nil + ) do { - let params = DocumentColorParams( - textDocument: TextDocumentIdentifier(uri: documentURI), - workDoneToken: nil, - partialResultToken: nil - ) return try await lspInstance.documentColor(params) } catch { - print("requestDocumentColor Error \(error)") + logger.warning("requestDocumentColor: Error \(error)") + throw error } - return [] } } diff --git a/CodeEdit/Features/LSP/LanguageServerExtensions/LanguageServer+DocumentHighlight.swift b/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+DocumentHighlight.swift similarity index 77% rename from CodeEdit/Features/LSP/LanguageServerExtensions/LanguageServer+DocumentHighlight.swift rename to CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+DocumentHighlight.swift index dec93142c..4bd66e649 100644 --- a/CodeEdit/Features/LSP/LanguageServerExtensions/LanguageServer+DocumentHighlight.swift +++ b/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+DocumentHighlight.swift @@ -11,10 +11,10 @@ extension LanguageServer { /// The document highlight request is sent from the client to the server to resolve document /// highlights for a given text document position. For programming languages this usually /// highlights all references to the symbol scoped to this file. - func requestDocumentHighlight( - document documentURI: String, - _ position: Position - ) async -> DocumentHighlightResponse { + func requestHighlight( + for documentURI: String, + position: Position + ) async throws -> DocumentHighlightResponse { do { let params = DocumentHighlightParams( textDocument: TextDocumentIdentifier(uri: documentURI), @@ -24,9 +24,8 @@ extension LanguageServer { ) return try await lspInstance.documentHighlight(params) } catch { - print("requestDocumentHighlight Error: \(error)") + logger.warning("requestDocumentHighlight Error: \(error)") + throw error } - - return nil } } diff --git a/CodeEdit/Features/LSP/LanguageServerExtensions/LanguageServer+DocumentLink.swift b/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+DocumentLink.swift similarity index 60% rename from CodeEdit/Features/LSP/LanguageServerExtensions/LanguageServer+DocumentLink.swift rename to CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+DocumentLink.swift index 075729f91..fafd7f2f7 100644 --- a/CodeEdit/Features/LSP/LanguageServerExtensions/LanguageServer+DocumentLink.swift +++ b/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+DocumentLink.swift @@ -11,12 +11,13 @@ import LanguageServerProtocol // TODO: DocumentLinkParams IS MISSING `textDocument: TextDocumentIdentifier;` FIELD IN LSP LIBRARY extension LanguageServer { - func requestDocumentLinkResolve(_ documentLink: DocumentLink) async -> DocumentLink? { + @available(*, deprecated, message: "Not functional, see comment.") + func requestLinkResolve(_ documentLink: DocumentLink) async throws -> DocumentLink? { do { return try await lspInstance.documentLinkResolve(documentLink) } catch { - print("requestDocumentLinkResolve Error: \(error)") + logger.warning("requestDocumentLinkResolve: Error \(error)") + throw error } - return nil } } diff --git a/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+DocumentSymbol.swift b/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+DocumentSymbol.swift new file mode 100644 index 000000000..4a50aabbb --- /dev/null +++ b/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+DocumentSymbol.swift @@ -0,0 +1,22 @@ +// +// LanguageServer+DocumentSymbol.swift +// CodeEdit +// +// Created by Abe Malla on 2/7/24. +// + +import Foundation +import LanguageServerProtocol + +extension LanguageServer { + func requestSymbols(for documentURI: String) async throws -> DocumentSymbolResponse { + do { + let textDocumentIdentifier = TextDocumentIdentifier(uri: documentURI) + let documentSymbolParams = DocumentSymbolParams(textDocument: textDocumentIdentifier) + return try await lspInstance.documentSymbol(documentSymbolParams) + } catch { + logger.warning("requestSymbols: Error \(error)") + throw error + } + } +} diff --git a/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+DocumentSync.swift b/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+DocumentSync.swift new file mode 100644 index 000000000..e78bf7e5b --- /dev/null +++ b/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+DocumentSync.swift @@ -0,0 +1,140 @@ +// +// LanguageServer+DocumentSync.swift +// CodeEdit +// +// Created by Abe Malla on 2/7/24. +// + +import Foundation +import LanguageServerProtocol + +extension LanguageServer { + // swiftlint:disable line_length + /// Determines the type of document sync the server supports. + /// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_synchronization_sc + fileprivate func resolveDocumentSyncKind() -> TextDocumentSyncKind { + // swiftlint:enable line_length + var syncKind: TextDocumentSyncKind = .none + switch serverCapabilities.textDocumentSync { + case .optionA(let options): + syncKind = options.change ?? .none + case .optionB(let kind): + syncKind = kind + default: + syncKind = .none + } + return syncKind + } + + /// Determines whether or not the server supports document tracking. + fileprivate func resolveOpenCloseSupport() -> Bool { + switch serverCapabilities.textDocumentSync { + case .optionA(let options): + return options.openClose ?? false + case .optionB: + return true + default: + return true + } + } + + // Used to avoid a lint error (`large_tuple`) for the return type of `getIsolatedDocumentContent` + fileprivate struct DocumentContent { + let uri: String + let language: LanguageIdentifier + let content: String + } + + /// Tells the language server we've opened a document and would like to begin working with it. + /// - Parameter document: The code document to open. + /// - Throws: Throws errors produced by the language server connection. + func openDocument(_ document: CodeFileDocument) async throws { + do { + guard resolveOpenCloseSupport(), let content = await getIsolatedDocumentContent(document) else { + return + } + logger.debug("Opening Document \(content.uri, privacy: .private)") + + self.openFiles.addDocument(document) + + let textDocument = TextDocumentItem( + uri: content.uri, + languageId: content.language, + version: 0, + text: content.content + ) + try await lspInstance.textDocumentDidOpen(DidOpenTextDocumentParams(textDocument: textDocument)) + } catch { + logger.warning("addDocument: Error \(error)") + throw error + } + } + + /// Helper function for grabbing a document's content from the main actor. + @MainActor + private func getIsolatedDocumentContent(_ document: CodeFileDocument) -> DocumentContent? { + guard let uri = document.languageServerURI, + let language = document.getLanguage().lspLanguage, + let content = document.content?.string else { + return nil + } + return DocumentContent(uri: uri, language: language, content: content) + } + + /// Stops tracking a file and notifies the language server. + /// - Parameter uri: The URI of the document to close. + /// - Throws: Throws errors produced by the language server connection. + func closeDocument(_ uri: String) async throws { + do { + guard resolveOpenCloseSupport() && openFiles.document(for: uri) != nil else { return } + logger.debug("Closing document \(uri, privacy: .private)") + openFiles.removeDocument(for: uri) + let params = DidCloseTextDocumentParams(textDocument: TextDocumentIdentifier(uri: uri)) + try await lspInstance.textDocumentDidClose(params) + } catch { + logger.warning("closeDocument: Error \(error)") + throw error + } + } + + /// Updates the document with the specified URI with new text and increments its version. + /// - Parameters: + /// - uri: The URI of the document to update. + /// - range: The range being replaced. + /// - string: The string being inserted into the replacement range. + /// - Throws: Throws errors produced by the language server connection. + func documentChanged( + uri: String, + replacedContentIn range: LSPRange, + with string: String + ) async throws { + do { + logger.debug("Document updated, \(uri, privacy: .private)") + switch resolveDocumentSyncKind() { + case .full: + guard let file = openFiles.document(for: uri) else { return } + let content = await MainActor.run { + let storage = file.content + return storage?.string + } + guard let content else { return } + let changeEvent = TextDocumentContentChangeEvent(range: nil, rangeLength: nil, text: content) + try await lspInstance.textDocumentDidChange( + DidChangeTextDocumentParams(uri: uri, version: 0, contentChange: changeEvent) + ) + case .incremental: + let fileVersion = openFiles.incrementVersion(for: uri) + // rangeLength is depreciated in the LSP spec. + let changeEvent = TextDocumentContentChangeEvent(range: range, rangeLength: nil, text: string) + try await lspInstance.textDocumentDidChange( + DidChangeTextDocumentParams(uri: uri, version: fileVersion, contentChange: changeEvent) + ) + case .none: + return + } + } catch { + logger.warning("closeDocument: Error \(error)") + throw error + } + } +} diff --git a/CodeEdit/Features/LSP/LanguageServerExtensions/LanguageServer+FoldingRange.swift b/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+FoldingRange.swift similarity index 66% rename from CodeEdit/Features/LSP/LanguageServerExtensions/LanguageServer+FoldingRange.swift rename to CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+FoldingRange.swift index 0e0c2b5b6..c4a35c012 100644 --- a/CodeEdit/Features/LSP/LanguageServerExtensions/LanguageServer+FoldingRange.swift +++ b/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+FoldingRange.swift @@ -9,14 +9,13 @@ import Foundation import LanguageServerProtocol extension LanguageServer { - func requestFoldingRange(document documentURI: String) async -> FoldingRangeResponse { + func requestFoldingRange(for documentURI: String) async throws -> FoldingRangeResponse { do { let params = FoldingRangeParams(textDocument: TextDocumentIdentifier(uri: documentURI)) return try await lspInstance.foldingRange(params) } catch { - // TODO: LOGGING - print("requestFoldingRange Error: \(error)") + logger.warning("requestFoldingRange: Error \(error)") + throw error } - return nil } } diff --git a/CodeEdit/Features/LSP/LanguageServerExtensions/LanguageServer+Formatting.swift b/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+Formatting.swift similarity index 75% rename from CodeEdit/Features/LSP/LanguageServerExtensions/LanguageServer+Formatting.swift rename to CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+Formatting.swift index 4de818e6c..fd0d810a8 100644 --- a/CodeEdit/Features/LSP/LanguageServerExtensions/LanguageServer+Formatting.swift +++ b/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+Formatting.swift @@ -8,13 +8,11 @@ import Foundation import LanguageServerProtocol -// TODO: LOGGING - extension LanguageServer { func requestFormatting( - document documentURI: String, + for documentURI: String, withFormat formattingOptions: FormattingOptions - ) async -> FormattingResult { + ) async throws -> FormattingResult { do { let params = DocumentFormattingParams( textDocument: TextDocumentIdentifier(uri: documentURI), @@ -22,16 +20,16 @@ extension LanguageServer { ) return try await lspInstance.formatting(params) } catch { - print("requestFormatting Error \(error)") + logger.warning("requestFormatting: Error \(error)") + throw error } - return [] } func requestRangeFormatting( - document documentURI: String, + for documentURI: String, _ range: LSPRange, withFormat formattingOptions: FormattingOptions - ) async -> FormattingResult { + ) async throws -> FormattingResult { do { let params = DocumentRangeFormattingParams( textDocument: TextDocumentIdentifier(uri: documentURI), @@ -40,17 +38,17 @@ extension LanguageServer { ) return try await lspInstance.rangeFormatting(params) } catch { - print("requestRangeFormatting Error \(error)") + logger.warning("requestRangeFormatting: Error \(error)") + throw error } - return [] } func requestOnTypeFormatting( - document documentURI: String, + for documentURI: String, _ position: Position, character char: String, withFormat formattingOptions: FormattingOptions - ) async -> FormattingResult { + ) async throws -> FormattingResult { do { let params = DocumentOnTypeFormattingParams( textDocument: TextDocumentIdentifier(uri: documentURI), @@ -60,8 +58,8 @@ extension LanguageServer { ) return try await lspInstance.onTypeFormatting(params) } catch { - print("requestOnTypeFormatting Error \(error)") + logger.warning("requestOnTypeFormatting: Error \(error)") + throw error } - return [] } } diff --git a/CodeEdit/Features/LSP/LanguageServerExtensions/LanguageServer+Hover.swift b/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+Hover.swift similarity index 75% rename from CodeEdit/Features/LSP/LanguageServerExtensions/LanguageServer+Hover.swift rename to CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+Hover.swift index 01c213d56..5a11ca0a9 100644 --- a/CodeEdit/Features/LSP/LanguageServerExtensions/LanguageServer+Hover.swift +++ b/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+Hover.swift @@ -11,7 +11,7 @@ import LanguageServerProtocol extension LanguageServer { /// The hover request is sent from the client to the server to request hover /// information at a given text document position. - func requestHover(document documentURI: String, _ position: Position) async -> HoverResponse { + func requestHover(for documentURI: String, _ position: Position) async throws -> HoverResponse { do { let params = TextDocumentPositionParams( textDocument: TextDocumentIdentifier(uri: documentURI), @@ -19,8 +19,8 @@ extension LanguageServer { ) return try await lspInstance.hover(params) } catch { - print("requestHover Error \(error)") + logger.warning("requestHover: Error \(error)") + throw error } - return nil } } diff --git a/CodeEdit/Features/LSP/LanguageServerExtensions/LanguageServer+Implementation.swift b/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+Implementation.swift similarity index 72% rename from CodeEdit/Features/LSP/LanguageServerExtensions/LanguageServer+Implementation.swift rename to CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+Implementation.swift index b22f97848..87c7a424a 100644 --- a/CodeEdit/Features/LSP/LanguageServerExtensions/LanguageServer+Implementation.swift +++ b/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+Implementation.swift @@ -10,7 +10,7 @@ import LanguageServerProtocol extension LanguageServer { /// Resolve the implementation location of a symbol at a given text document position - func requestImplementation(document documentURI: String, _ position: Position) async -> ImplementationResponse { + func requestImplementation(for documentURI: String, _ position: Position) async throws -> ImplementationResponse { do { let params = TextDocumentPositionParams( textDocument: TextDocumentIdentifier(uri: documentURI), @@ -18,8 +18,8 @@ extension LanguageServer { ) return try await lspInstance.implementation(params) } catch { - print("requestImplementation Error \(error)") + logger.warning("requestImplementation: Error \(error)") + throw error } - return nil } } diff --git a/CodeEdit/Features/LSP/LanguageServerExtensions/LanguageServer+InlayHint.swift b/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+InlayHint.swift similarity index 73% rename from CodeEdit/Features/LSP/LanguageServerExtensions/LanguageServer+InlayHint.swift rename to CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+InlayHint.swift index 2bc9cbca2..7725c57a2 100644 --- a/CodeEdit/Features/LSP/LanguageServerExtensions/LanguageServer+InlayHint.swift +++ b/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+InlayHint.swift @@ -11,7 +11,7 @@ import LanguageServerProtocol extension LanguageServer { /// Compute inlay hints for a given [text document, range] tuple that may be rendered in the /// editor in place with other text - func requestInlayHint(document documentURI: String, _ range: LSPRange) async -> InlayHintResponse { + func requestInlayHint(for documentURI: String, _ range: LSPRange) async throws -> InlayHintResponse { do { let params = InlayHintParams( workDoneToken: nil, @@ -20,20 +20,20 @@ extension LanguageServer { ) return try await lspInstance.inlayHint(params) } catch { - print("requestInlayHint Error \(error)") + logger.warning("requestInlayHint: Error \(error)") + throw error } - return nil } /// The request is sent from the client to the server to resolve additional information for a given inlay hint. /// This is usually used to compute the tooltip, location or command properties of an inlay hint’s label part /// to avoid its unnecessary computation during the textDocument/inlayHint request. - func requestInlayHintResolve(_ inlayHint: InlayHint) async -> InlayHint? { + func requestInlayHintResolve(_ inlayHint: InlayHint) async throws -> InlayHint? { do { return try await lspInstance.inlayHintResolve(inlayHint) } catch { - print("requestInlayHint Error \(error)") + logger.warning("requestInlayHintResolve: Error \(error)") + throw error } - return nil } } diff --git a/CodeEdit/Features/LSP/LanguageServerExtensions/LanguageServer+References.swift b/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+References.swift similarity index 80% rename from CodeEdit/Features/LSP/LanguageServerExtensions/LanguageServer+References.swift rename to CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+References.swift index 56c8737cf..48ca34d03 100644 --- a/CodeEdit/Features/LSP/LanguageServerExtensions/LanguageServer+References.swift +++ b/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+References.swift @@ -11,10 +11,10 @@ import LanguageServerProtocol extension LanguageServer { /// Resolve project-wide references for the symbol denoted by the given text document position func requestFindReferences( - document documentURI: String, + for documentURI: String, _ position: Position, _ includeDeclaration: Bool = false - ) async -> ReferenceResponse { + ) async throws -> ReferenceResponse { do { let params = ReferenceParams( textDocument: TextDocumentIdentifier(uri: documentURI), @@ -23,8 +23,8 @@ extension LanguageServer { ) return try await lspInstance.references(params) } catch { - print("requestInlayHint Error \(error)") + logger.warning("requestFindReferences: Error \(error)") + throw error } - return nil } } diff --git a/CodeEdit/Features/LSP/LanguageServerExtensions/LanguageServer+Rename.swift b/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+Rename.swift similarity index 71% rename from CodeEdit/Features/LSP/LanguageServerExtensions/LanguageServer+Rename.swift rename to CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+Rename.swift index 7fa5e3f2c..bed869c0e 100644 --- a/CodeEdit/Features/LSP/LanguageServerExtensions/LanguageServer+Rename.swift +++ b/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+Rename.swift @@ -9,7 +9,7 @@ import LanguageServerProtocol extension LanguageServer { /// Setup and test the validity of a rename operation at a given location - func requestPrepareRename(document documentURI: String, _ position: Position) async -> PrepareRenameResponse { + func requestPrepareRename(for documentURI: String, _ position: Position) async throws -> PrepareRenameResponse { do { let params = TextDocumentPositionParams( textDocument: TextDocumentIdentifier(uri: documentURI), @@ -17,17 +17,17 @@ extension LanguageServer { ) return try await lspInstance.prepareRename(params) } catch { - print("requestInlayHint Error \(error)") + logger.warning("requestPrepareRename: Error \(error)") + throw error } - return nil } /// Ask the server to compute a workspace change so that the client can perform a workspace-wide rename of a symbol func requestRename( - document documentURI: String, - _ position: Position, + for documentURI: String, + position: Position, newName name: String - ) async -> RenameResponse { + ) async throws -> RenameResponse { do { let params = RenameParams( textDocument: TextDocumentIdentifier(uri: documentURI), @@ -36,8 +36,8 @@ extension LanguageServer { ) return try await lspInstance.rename(params) } catch { - print("requestInlayHint Error \(error)") + logger.warning("requestRename: Error \(error)") + throw error } - return nil } } diff --git a/CodeEdit/Features/LSP/LanguageServerExtensions/LanguageServer+SelectionRange.swift b/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+SelectionRange.swift similarity index 72% rename from CodeEdit/Features/LSP/LanguageServerExtensions/LanguageServer+SelectionRange.swift rename to CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+SelectionRange.swift index 328c8f562..06585932b 100644 --- a/CodeEdit/Features/LSP/LanguageServerExtensions/LanguageServer+SelectionRange.swift +++ b/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+SelectionRange.swift @@ -10,7 +10,7 @@ import LanguageServerProtocol extension LanguageServer { /// Setup and test the validity of a rename operation at a given location - func requestSelectionRange(document documentURI: String, _ positions: [Position]) async -> SelectionRangeResponse { + func requestSelectionRange(for documentURI: String, positions: [Position]) async throws -> SelectionRangeResponse { do { let params = SelectionRangeParams( workDoneToken: nil, @@ -19,8 +19,8 @@ extension LanguageServer { ) return try await lspInstance.selectionRange(params) } catch { - print("requestInlayHint Error \(error)") + logger.warning("requestSelectionRange: Error \(error)") + throw error } - return nil } } diff --git a/CodeEdit/Features/LSP/LanguageServerExtensions/LanguageServer+SemanticTokens.swift b/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+SemanticTokens.swift similarity index 60% rename from CodeEdit/Features/LSP/LanguageServerExtensions/LanguageServer+SemanticTokens.swift rename to CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+SemanticTokens.swift index 76e5b1396..02cb29947 100644 --- a/CodeEdit/Features/LSP/LanguageServerExtensions/LanguageServer+SemanticTokens.swift +++ b/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+SemanticTokens.swift @@ -10,32 +10,35 @@ import LanguageServerProtocol extension LanguageServer { /// Setup and test the validity of a rename operation at a given location - func requestSemanticTokensFull(document documentURI: String) async -> SemanticTokensResponse { + func requestSemanticTokens(for documentURI: String) async throws -> SemanticTokensResponse { do { let params = SemanticTokensParams( textDocument: TextDocumentIdentifier(uri: documentURI) ) return try await lspInstance.semanticTokensFull(params) } catch { - print("requestInlayHint Error \(error)") + logger.warning("requestSemanticTokens full: Error \(error)") + throw error } - return nil } - func requestSemanticTokensRange(document documentURI: String, _ range: LSPRange) async -> SemanticTokensResponse { + func requestSemanticTokens( + for documentURI: String, + forRange range: LSPRange + ) async throws -> SemanticTokensResponse { do { let params = SemanticTokensRangeParams(textDocument: TextDocumentIdentifier(uri: documentURI), range: range) return try await lspInstance.semanticTokensRange(params) } catch { - print("requestInlayHint Error \(error)") + logger.warning("requestSemanticTokens range: Error \(error)") + throw error } - return nil } - func requestSemanticTokensFullDelta( - document documentURI: String, - _ previousResultId: String - ) async -> SemanticTokensDeltaResponse { + func requestSemanticTokens( + for documentURI: String, + previousResultId: String + ) async throws -> SemanticTokensDeltaResponse { do { let params = SemanticTokensDeltaParams( textDocument: TextDocumentIdentifier(uri: documentURI), @@ -43,8 +46,8 @@ extension LanguageServer { ) return try await lspInstance.semanticTokensFullDelta(params) } catch { - print("requestInlayHint Error \(error)") + logger.warning("requestSemanticTokens versioned: Error \(error)") + throw error } - return nil } } diff --git a/CodeEdit/Features/LSP/LanguageServerExtensions/LanguageServer+SignatureHelp.swift b/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+SignatureHelp.swift similarity index 72% rename from CodeEdit/Features/LSP/LanguageServerExtensions/LanguageServer+SignatureHelp.swift rename to CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+SignatureHelp.swift index 1fd3c991b..b9031d328 100644 --- a/CodeEdit/Features/LSP/LanguageServerExtensions/LanguageServer+SignatureHelp.swift +++ b/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+SignatureHelp.swift @@ -10,7 +10,7 @@ import LanguageServerProtocol extension LanguageServer { /// Request signature information at a given cursor position - func requestSignatureHelp(document documentURI: String, _ position: Position) async -> SignatureHelpResponse { + func requestSignatureHelp(for documentURI: String, _ position: Position) async throws -> SignatureHelpResponse { do { let params = TextDocumentPositionParams( textDocument: TextDocumentIdentifier(uri: documentURI), @@ -18,8 +18,8 @@ extension LanguageServer { ) return try await lspInstance.signatureHelp(params) } catch { - print("requestInlayHint Error \(error)") + logger.warning("requestInlayHint: Error \(error)") + throw error } - return nil } } diff --git a/CodeEdit/Features/LSP/LanguageServerExtensions/LanguageServer+TypeDefinition.swift b/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+TypeDefinition.swift similarity index 72% rename from CodeEdit/Features/LSP/LanguageServerExtensions/LanguageServer+TypeDefinition.swift rename to CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+TypeDefinition.swift index 3b68a6a5b..868ea023c 100644 --- a/CodeEdit/Features/LSP/LanguageServerExtensions/LanguageServer+TypeDefinition.swift +++ b/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+TypeDefinition.swift @@ -10,7 +10,7 @@ import LanguageServerProtocol extension LanguageServer { /// Resolve the type definition location of a symbol at a given text document position - func requestTypeDefinition(document documentURI: String, _ position: Position) async -> TypeDefinitionResponse { + func requestTypeDefinition(for documentURI: String, _ position: Position) async throws -> TypeDefinitionResponse { do { let params = TextDocumentPositionParams( textDocument: TextDocumentIdentifier(uri: documentURI), @@ -18,8 +18,8 @@ extension LanguageServer { ) return try await lspInstance.typeDefinition(params) } catch { - print("requestInlayHint Error \(error)") + logger.warning("requestTypeDefinition: Error \(error)") + throw error } - return nil } } diff --git a/CodeEdit/Features/LSP/LSPCache+Data.swift b/CodeEdit/Features/LSP/LanguageServer/LSPCache+Data.swift similarity index 100% rename from CodeEdit/Features/LSP/LSPCache+Data.swift rename to CodeEdit/Features/LSP/LanguageServer/LSPCache+Data.swift diff --git a/CodeEdit/Features/LSP/LSPCache.swift b/CodeEdit/Features/LSP/LanguageServer/LSPCache.swift similarity index 98% rename from CodeEdit/Features/LSP/LSPCache.swift rename to CodeEdit/Features/LSP/LanguageServer/LSPCache.swift index 7045fc7b0..424bbf6d9 100644 --- a/CodeEdit/Features/LSP/LSPCache.swift +++ b/CodeEdit/Features/LSP/LanguageServer/LSPCache.swift @@ -7,7 +7,7 @@ import Foundation -final class LSPCache { +class LSPCache { private var cache = NSCache() func get(key: CacheKey, as type: T.Type) -> T? { diff --git a/CodeEdit/Features/LSP/LanguageServer.swift b/CodeEdit/Features/LSP/LanguageServer/LanguageServer.swift similarity index 62% rename from CodeEdit/Features/LSP/LanguageServer.swift rename to CodeEdit/Features/LSP/LanguageServer/LanguageServer.swift index d5664c39d..9f00b4f4c 100644 --- a/CodeEdit/Features/LSP/LanguageServer.swift +++ b/CodeEdit/Features/LSP/LanguageServer/LanguageServer.swift @@ -9,8 +9,12 @@ import JSONRPC import Foundation import LanguageClient import LanguageServerProtocol +import OSLog + +class LanguageServer { + static let logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "", category: "LanguageServer") + let logger: Logger -struct LanguageServer { /// Identifies which language the server belongs to let languageId: LanguageIdentifier /// Holds information about the language server binary @@ -18,55 +22,91 @@ struct LanguageServer { /// A cache to hold responses from the server, to minimize duplicate server requests let lspCache = LSPCache() - // TODO: REMOVE WHEN NEW DOCUMENT TRACKER IS IMPLEMENTED. IS PART OF NEW FILE SOLUTION. - var trackedDocuments: [String: TextDocumentItem] = [:] + let openFiles: LanguageServerFileMap + + /// The configuration options this server supports. + var serverCapabilities: ServerCapabilities /// An instance of a language server, that may or may not be initialized private(set) var lspInstance: InitializingServer /// The path to the root of the project private(set) var rootPath: URL + init( + languageId: LanguageIdentifier, + binary: LanguageServerBinary, + lspInstance: InitializingServer, + serverCapabilities: ServerCapabilities, + rootPath: URL + ) { + self.languageId = languageId + self.binary = binary + self.lspInstance = lspInstance + self.serverCapabilities = serverCapabilities + self.rootPath = rootPath + self.openFiles = LanguageServerFileMap() + self.logger = Logger( + subsystem: Bundle.main.bundleIdentifier ?? "", + category: "LanguageServer.\(languageId.rawValue)" + ) + } + + /// Creates and initializes a language server. + /// - Parameters: + /// - languageId: The id of the language to create. + /// - binary: The binary where the language server is stored. + /// - workspacePath: The path of the workspace being opened. + /// - Returns: An initialized language server. static func createServer( for languageId: LanguageIdentifier, with binary: LanguageServerBinary, - rootPath: URL, - workspaceFolders: [WorkspaceFolder]? - ) throws -> Self { + workspacePath: String + ) async throws -> LanguageServer { let executionParams = Process.ExecutionParameters( path: binary.execPath, arguments: binary.args, environment: binary.env ) - var channel: DataChannel? + let server = InitializingServer( + server: try makeLocalServerConnection(languageId: languageId, executionParams: executionParams), + initializeParamsProvider: getInitParams(workspacePath: workspacePath) + ) + let capabilities = try await server.initializeIfNeeded() + return LanguageServer( + languageId: languageId, + binary: binary, + lspInstance: server, + serverCapabilities: capabilities, + rootPath: URL(filePath: workspacePath) + ) + } + + /// Creates a data channel for sending and receiving data with an LSP. + /// - Parameters: + /// - languageId: The ID of the language to create the channel for. + /// - executionParams: The parameters for executing the local process. + /// - Returns: A new connection to the language server. + static func makeLocalServerConnection( + languageId: LanguageIdentifier, + executionParams: Process.ExecutionParameters + ) throws -> JSONRPCServerConnection { do { - channel = try DataChannel.localProcessChannel( + let channel = try DataChannel.localProcessChannel( parameters: executionParams, terminationHandler: { - print("Terminated \(languageId)") + logger.debug("Terminated data channel for \(languageId.rawValue)") } ) + return JSONRPCServerConnection(dataChannel: channel) } catch { + logger.warning("Failed to initialize data channel for \(languageId.rawValue)") throw error } - guard let channel = channel else { - throw NSError( - domain: "LanguageClient", - code: 1, - userInfo: [NSLocalizedDescriptionKey: "Failed to start server for language \(languageId.rawValue)"] - ) - } - - let localServer = LanguageServerProtocol.JSONRPCServerConnection(dataChannel: channel) - let server = InitializingServer( - server: localServer, - initializeParamsProvider: getInitParams(projectURL: rootPath) - ) - return LanguageServer(languageId: languageId, binary: binary, lspInstance: server, rootPath: rootPath) } // swiftlint:disable function_body_length - private static func getInitParams(projectURL: URL) -> InitializingServer.InitializeParamsProvider { + static func getInitParams(workspacePath: String) -> InitializingServer.InitializeParamsProvider { let provider: InitializingServer.InitializeParamsProvider = { // Text Document Capabilities let textDocumentCapabilities = TextDocumentClientCapabilities( @@ -92,6 +132,19 @@ struct LanguageServer { completionList: CompletionClientCapabilities.CompletionList( itemDefaults: ["default1", "default2"] ) + ), + // swiftlint:disable:next line_length + // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#semanticTokensClientCapabilities + semanticTokens: SemanticTokensClientCapabilities( + dynamicRegistration: true, + requests: .init(range: true, delta: false), + tokenTypes: [], + tokenModifiers: [], + formats: [.relative], + overlappingTokenSupport: true, + multilineTokenSupport: true, + serverCancelSupport: true, + augmentsSyntaxTokens: false ) ) @@ -150,7 +203,7 @@ struct LanguageServer { processId: nil, locale: nil, rootPath: nil, - rootUri: projectURL.absoluteString, + rootUri: workspacePath, initializationOptions: [], capabilities: capabilities, trace: nil, @@ -161,19 +214,9 @@ struct LanguageServer { // swiftlint:enable function_body_length } - /// Initializes the language server if it hasn't been initialized already. - public func initialize() async throws { - do { - _ = try await lspInstance.initializeIfNeeded() - print("Language server for \(languageId.rawValue) initialized successfully") - } catch { - print("Failed to initialize \(languageId.rawValue) LSP instance: \(error.localizedDescription)") - throw error - } - } - /// Shuts down the language server and exits it. public func shutdown() async throws { + self.logger.info("Shutting down language server") try await lspInstance.shutdownAndExit() } } diff --git a/CodeEdit/Features/LSP/LanguageServer/LanguageServerFileMap.swift b/CodeEdit/Features/LSP/LanguageServer/LanguageServerFileMap.swift new file mode 100644 index 000000000..0f3d4469f --- /dev/null +++ b/CodeEdit/Features/LSP/LanguageServer/LanguageServerFileMap.swift @@ -0,0 +1,62 @@ +// +// LanguageServerFileMap.swift +// CodeEdit +// +// Created by Khan Winter on 9/8/24. +// + +import Foundation +import LanguageServerProtocol + +class LanguageServerFileMap { + private var trackedDocuments: NSMapTable + private var trackedDocumentVersions: [String: Int] = [:] + + init() { + trackedDocuments = NSMapTable(valueOptions: [.weakMemory]) + } + + // MARK: - Track & Remove Documents + + func addDocument(_ document: CodeFileDocument) { + guard let uri = document.languageServerURI else { return } + trackedDocuments.setObject(document, forKey: uri as NSString) + trackedDocumentVersions[uri] = 0 + } + + func document(for uri: DocumentUri) -> CodeFileDocument? { + let url = URL(filePath: uri) + return trackedDocuments.object(forKey: url.languageServerURI as NSString) + } + + func removeDocument(for document: CodeFileDocument) { + guard let uri = document.languageServerURI else { return } + removeDocument(for: uri) + } + + func removeDocument(for uri: DocumentUri) { + trackedDocuments.removeObject(forKey: uri as NSString) + trackedDocumentVersions.removeValue(forKey: uri) + } + + // MARK: - Version Number Tracking + + func incrementVersion(for document: CodeFileDocument) -> Int { + guard let uri = document.languageServerURI else { return 0 } + return incrementVersion(for: uri) + } + + func incrementVersion(for uri: DocumentUri) -> Int { + trackedDocumentVersions[uri] = (trackedDocumentVersions[uri] ?? 0) + 1 + return trackedDocumentVersions[uri] ?? 0 + } + + func documentVersion(for document: CodeFileDocument) -> Int? { + guard let uri = document.languageServerURI else { return nil } + return documentVersion(for: uri) + } + + func documentVersion(for uri: DocumentUri) -> Int? { + return trackedDocumentVersions[uri] + } +} diff --git a/CodeEdit/Features/LSP/LanguageServerExtensions/LanguageServer+CallHierarchy.swift b/CodeEdit/Features/LSP/LanguageServerExtensions/LanguageServer+CallHierarchy.swift deleted file mode 100644 index d01227a79..000000000 --- a/CodeEdit/Features/LSP/LanguageServerExtensions/LanguageServer+CallHierarchy.swift +++ /dev/null @@ -1,70 +0,0 @@ -// -// LanguageServer+CallHierarchy.swift -// CodeEdit -// -// Created by Abe Malla on 2/7/24. -// - -import Foundation -import LanguageServerProtocol - -extension LanguageServer { - func requestPrepareCallHierarchy( - document documentURI: String, _ position: Position - ) async -> CallHierarchyPrepareResponse { - let prepareParams = CallHierarchyPrepareParams( - textDocument: TextDocumentIdentifier(uri: documentURI), - position: position, - workDoneToken: nil - ) - - do { - guard let items = try await lspInstance.prepareCallHierarchy(prepareParams) else { - return [] - } - return items - } catch { - print("requestPrepareCallHierarchy: Error \(error)") - } - - return [] - } - - func requestCallHierarchyIncomingCalls( - _ callHierarchyItem: CallHierarchyItem - ) async -> CallHierarchyIncomingCallsResponse { - let incomingParams = CallHierarchyIncomingCallsParams( - item: callHierarchyItem, - workDoneToken: nil - ) - - do { - guard let incomingCalls = try await lspInstance.callHierarchyIncomingCalls(incomingParams) else { - return [] - } - return incomingCalls - } catch { - print("requestCallHierarchyIncomingCalls: Error \(error)") - } - return [] - } - - func requestCallHierarchyOutgoingCalls( - _ callHierarchyItem: CallHierarchyItem - ) async -> CallHierarchyOutgoingCallsResponse { - let outgoingParams = CallHierarchyOutgoingCallsParams( - item: callHierarchyItem, - workDoneToken: nil - ) - - do { - guard let outgoingCalls = try await lspInstance.callHierarchyOutgoingCalls(outgoingParams) else { - return [] - } - return outgoingCalls - } catch { - print("requestCallHierarchyOutgoingCalls: Error \(error)") - } - return [] - } -} diff --git a/CodeEdit/Features/LSP/LanguageServerExtensions/LanguageServer+ColorPresentation.swift b/CodeEdit/Features/LSP/LanguageServerExtensions/LanguageServer+ColorPresentation.swift deleted file mode 100644 index 070de5bfd..000000000 --- a/CodeEdit/Features/LSP/LanguageServerExtensions/LanguageServer+ColorPresentation.swift +++ /dev/null @@ -1,32 +0,0 @@ -// -// LanguageServer+ColorPresentation.swift -// CodeEdit -// -// Created by Abe Malla on 2/7/24. -// - -import Foundation -import LanguageServerProtocol - -extension LanguageServer { - func requestColorPresentation( - document documentURI: String, - _ color: Color, - _ range: LSPRange - ) async -> ColorPresentationResponse { - let params = ColorPresentationParams( - workDoneToken: nil, - partialResultToken: nil, - textDocument: TextDocumentIdentifier(uri: documentURI), - color: color, - range: range - ) - do { - return try await lspInstance.colorPresentation(params) - } catch { - // TODO: LOGGING - print("requestColorPresentation: Error \(error)") - } - return [] - } -} diff --git a/CodeEdit/Features/LSP/LanguageServerExtensions/LanguageServer+Completion.swift b/CodeEdit/Features/LSP/LanguageServerExtensions/LanguageServer+Completion.swift deleted file mode 100644 index 2b1a43a70..000000000 --- a/CodeEdit/Features/LSP/LanguageServerExtensions/LanguageServer+Completion.swift +++ /dev/null @@ -1,32 +0,0 @@ -// -// LanguageServer+Completion.swift -// CodeEdit -// -// Created by Abe Malla on 2/7/24. -// - -import Foundation -import LanguageServerProtocol - -extension LanguageServer { - func requestCompletion(document documentURI: String, position: Position) async throws -> CompletionResponse { - let cacheKey = CacheKey( - uri: documentURI, - requestType: "completion", - extraData: position - ) - if let cachedResponse: CompletionResponse = lspCache.get(key: cacheKey, as: CompletionResponse.self) { - return cachedResponse - } - let completionParams = CompletionParams( - uri: documentURI, - position: position, - triggerKind: .invoked, - triggerCharacter: nil - ) - let response = try await lspInstance.completion(completionParams) - - lspCache.set(key: cacheKey, value: response) - return response - } -} diff --git a/CodeEdit/Features/LSP/LanguageServerExtensions/LanguageServer+Definition.swift b/CodeEdit/Features/LSP/LanguageServerExtensions/LanguageServer+Definition.swift deleted file mode 100644 index 394eb2f35..000000000 --- a/CodeEdit/Features/LSP/LanguageServerExtensions/LanguageServer+Definition.swift +++ /dev/null @@ -1,36 +0,0 @@ -// -// LanguageServer+Definition.swift -// CodeEdit -// -// Created by Abe Malla on 2/7/24. -// - -import Foundation -import LanguageServerProtocol - -extension LanguageServer { - func requestGoToDefinition( - for languageId: LanguageIdentifier, - document documentURI: String, - position: Position - ) async throws -> DefinitionResponse { - let cacheKey = CacheKey( - uri: documentURI, - requestType: "goToDefinition", - extraData: NoExtraData() - ) - if let cachedResponse: DefinitionResponse = lspCache.get(key: cacheKey, as: DefinitionResponse.self) { - return cachedResponse - } - - let textDocumentIdentifier = TextDocumentIdentifier(uri: documentURI) - let textDocumentPositionParams = TextDocumentPositionParams( - textDocument: textDocumentIdentifier, - position: position - ) - let response = try await lspInstance.definition(textDocumentPositionParams) - - lspCache.set(key: cacheKey, value: response) - return response - } -} diff --git a/CodeEdit/Features/LSP/LanguageServerExtensions/LanguageServer+Diagnostics.swift b/CodeEdit/Features/LSP/LanguageServerExtensions/LanguageServer+Diagnostics.swift deleted file mode 100644 index 83bcda43f..000000000 --- a/CodeEdit/Features/LSP/LanguageServerExtensions/LanguageServer+Diagnostics.swift +++ /dev/null @@ -1,32 +0,0 @@ -// -// LanguageServer+Diagnostics.swift -// CodeEdit -// -// Created by Abe Malla on 2/7/24. -// - -import Foundation -import LanguageServerProtocol - -extension LanguageServer { - func requestPullDiagnostics(document documentURI: String) async throws -> DocumentDiagnosticReport { - let cacheKey = CacheKey( - uri: documentURI, - requestType: "diagnostics", - extraData: NoExtraData() - ) - if let cachedResponse: DocumentDiagnosticReport = lspCache.get( - key: cacheKey, as: DocumentDiagnosticReport.self - ) { - return cachedResponse - } - - let response = try await lspInstance.diagnostics( - DocumentDiagnosticParams( - textDocument: TextDocumentIdentifier(uri: documentURI) - ) - ) - lspCache.set(key: cacheKey, value: response) - return response - } -} diff --git a/CodeEdit/Features/LSP/LanguageServerExtensions/LanguageServer+DocumentSymbol.swift b/CodeEdit/Features/LSP/LanguageServerExtensions/LanguageServer+DocumentSymbol.swift deleted file mode 100644 index b224c7f4d..000000000 --- a/CodeEdit/Features/LSP/LanguageServerExtensions/LanguageServer+DocumentSymbol.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// LanguageServer+DocumentSymbol.swift -// CodeEdit -// -// Created by Abe Malla on 2/7/24. -// - -import Foundation -import LanguageServerProtocol - -extension LanguageServer { - func requestDocumentSymbols( - for languageId: LanguageIdentifier, - document documentURI: String - ) async throws -> DocumentSymbolResponse { - let textDocumentIdentifier = TextDocumentIdentifier(uri: documentURI) - let documentSymbolParams = DocumentSymbolParams(textDocument: textDocumentIdentifier) - return try await lspInstance.documentSymbol(documentSymbolParams) - } -} diff --git a/CodeEdit/Features/LSP/LanguageServerExtensions/LanguageServer+DocumentUtil.swift b/CodeEdit/Features/LSP/LanguageServerExtensions/LanguageServer+DocumentUtil.swift deleted file mode 100644 index 5dda0c77f..000000000 --- a/CodeEdit/Features/LSP/LanguageServerExtensions/LanguageServer+DocumentUtil.swift +++ /dev/null @@ -1,180 +0,0 @@ -// -// LanguageServer+DocumentUtil.swift -// CodeEdit -// -// Created by Abe Malla on 2/7/24. -// - -import Foundation -import LanguageServerProtocol - -extension LanguageServer { - mutating func addDocument(_ fileURL: URL) async -> Bool { - do { - let docContent = try String(contentsOf: fileURL) - let textDocument = TextDocumentItem( - uri: fileURL.absoluteString, - languageId: .rust, - version: 0, - text: docContent - ) - return await self.addDocument(textDocument) - } catch { - print("addDocument: An error occurred: \(error)") - } - - return false - } - - /// Adds a TextDocumentItem to the tracked files and notifies the language server - mutating func addDocument(_ document: TextDocumentItem) async -> Bool { - do { - // Keep track of the document - trackedDocuments[document.uri] = document - - // Send notification to server about our opened file - let params = DidOpenTextDocumentParams(textDocument: document) - try await lspInstance.textDocumentDidOpen(params) - return true - } catch { - print("addDocument: An error occurred: \(error)") - } - - return false - } - - /// Stops tracking a file and notifies the language server - mutating func closeDocument(_ uri: String) async -> Bool { - guard let document = trackedDocuments.removeValue(forKey: uri) else { return false } - - do { - let params = DidCloseTextDocumentParams(textDocument: - TextDocumentIdentifier(uri: document.uri)) - try await lspInstance.textDocumentDidClose(params) - return true - } catch { - print("closeDocument: An error occurred: \(error)") - } - return false - } - - /// Updates the document with the specified URI with new text and increments its version. - /// - Parameters: - /// - uri: The URI of the document to update. - /// - newText: The new text to be set for the document. - /// - Returns: `true` if the document was successfully updated, `false` - mutating func updateDocument( - withUri uri: String, - newText: String, - range: LSPRange, - rangeLength: Int - ) async -> Bool { - // Update the document objects values, including the version - guard let currentDocument = trackedDocuments[uri], - let nsRange = convertLSPRangeToNSRange(range, in: currentDocument.text), - let stringRange = currentDocument.text.range(from: nsRange) else { - // TODO: LOG HERE - return false - } - - // Update the document's content and increment the version. - var updatedText = currentDocument.text - updatedText.replaceSubrange(stringRange, with: newText) - let updatedVersion = currentDocument.version + 1 - let updatedDocument = TextDocumentItem( - uri: currentDocument.uri, - languageId: currentDocument.languageId, - version: updatedVersion, - text: updatedText - ) - trackedDocuments[uri] = updatedDocument - - // Notify the server - do { - let change = TextDocumentContentChangeEvent( - range: range, - rangeLength: rangeLength, - text: newText - ) - let params = DidChangeTextDocumentParams( - textDocument: VersionedTextDocumentIdentifier(uri: uri, version: updatedVersion), - contentChanges: [change] - ) - try await lspInstance.textDocumentDidChange(params) - } catch { - print("updateDocument: An error occurred: \(error)") - } - return true - } -} - -fileprivate extension String { - func range(from nsRange: NSRange) -> Range? { - guard let range = Range(nsRange, in: self) else { return nil } - return range - } -} - -private func applyEditsToDocument(document: TextDocumentItem, edits: [TextEdit]) -> TextDocumentItem { - // Sort edits in reverse order to prevent offset issues - let sortedEdits = edits.sorted { $0.range.start > $1.range.start } - var updatedText = document.text - for edit in sortedEdits { - // Apply each edit to the document text - guard let nsRange = convertLSPRangeToNSRange(edit.range, in: updatedText), - let range = updatedText.range(from: nsRange) else { continue } - updatedText.replaceSubrange(range, with: edit.newText) - } - - return TextDocumentItem( - uri: document.uri, - languageId: document.languageId, - version: document.version, - text: updatedText - ) -} - -private func updateDocumentWithChanges( - document: TextDocumentItem, - changes: [TextDocumentContentChangeEvent] -) -> TextDocumentItem { - var updatedText = document.text - - for change in changes { - // Apply changes with range to the document text - if let lspRange = change.range, - let nsRange = convertLSPRangeToNSRange(lspRange, in: updatedText), - let range = updatedText.range(from: nsRange) { - updatedText.replaceSubrange(range, with: change.text) - } else { - // Replace the entire document text - updatedText = change.text - } - } - - return TextDocumentItem( - uri: document.uri, - languageId: document.languageId, - version: document.version, - text: updatedText - ) -} - -private func convertLSPRangeToNSRange(_ range: LSPRange, in text: String) -> NSRange? { - let lines = text.split(separator: "\n", omittingEmptySubsequences: false) - - // Calculate the start index - let startLineIndex = min(range.start.line, lines.count - 1) - let startCharacterIndex = min(range.start.character, lines[startLineIndex].count) - let startIndex = lines.prefix(startLineIndex).reduce(0, { $0 + $1.count + 1 }) + startCharacterIndex - - // Calculate the end index - let endLineIndex = min(range.end.line, lines.count - 1) - let endCharacterIndex = min(range.end.character, lines[endLineIndex].count) - let endIndex = lines.prefix(endLineIndex).reduce(0, { $0 + $1.count + 1 }) + endCharacterIndex - - // Ensure the range is valid - guard startIndex <= endIndex else { return nil } - - return NSRange(location: startIndex, length: endIndex - startIndex) -} diff --git a/CodeEdit/Features/LSP/Service/LSPService+Events.swift b/CodeEdit/Features/LSP/Service/LSPService+Events.swift new file mode 100644 index 000000000..b4baa73bb --- /dev/null +++ b/CodeEdit/Features/LSP/Service/LSPService+Events.swift @@ -0,0 +1,95 @@ +// +// LSPService+Events.swift +// CodeEdit +// +// Created by Abe Malla on 6/1/24. +// + +import LanguageClient +import LanguageServerProtocol + +extension LSPService { + func startListeningToEvents(for key: ClientKey) { + guard let languageClient = languageClients[key] else { + logger.error("Language client not found for \(key.languageId.rawValue)") + return + } + + // Create a new Task to listen to the events + let task = Task.detached { [weak self] in + for await event in languageClient.lspInstance.eventSequence { + await self?.handleEvent(event, for: key) + } + } + eventListeningTasks[key] = task + } + + func stopListeningToEvents(for key: ClientKey) { + if let task = eventListeningTasks[key] { + task.cancel() + eventListeningTasks.removeValue(forKey: key) + } + } + + private func handleEvent(_ event: ServerEvent, for key: ClientKey) { + // TODO: Handle Events +// switch event { +// case let .request(id, request): +// print("Request ID: \(id) for \(key.languageId.rawValue)") +// handleRequest(request) +// case let .notification(notification): +// handleNotification(notification) +// case let .error(error): +// print("Error from EventStream for \(key.languageId.rawValue): \(error)") +// } + } + + private func handleRequest(_ request: ServerRequest) { + // TODO: Handle Requests +// switch request { +// case let .workspaceConfiguration(params, _): +// print("workspaceConfiguration: \(params)") +// case let .workspaceFolders(handler): +// print("workspaceFolders: \(String(describing: handler))") +// case let .workspaceApplyEdit(params, _): +// print("workspaceApplyEdit: \(params)") +// case let .clientRegisterCapability(params, _): +// print("clientRegisterCapability: \(params)") +// case let .clientUnregisterCapability(params, _): +// print("clientUnregisterCapability: \(params)") +// case let .workspaceCodeLensRefresh(handler): +// print("workspaceCodeLensRefresh: \(String(describing: handler))") +// case let .workspaceSemanticTokenRefresh(handler): +// print("workspaceSemanticTokenRefresh: \(String(describing: handler))") +// case let .windowShowMessageRequest(params, _): +// print("windowShowMessageRequest: \(params)") +// case let .windowShowDocument(params, _): +// print("windowShowDocument: \(params)") +// case let .windowWorkDoneProgressCreate(params, _): +// print("windowWorkDoneProgressCreate: \(params)") +// +// default: +// print() +// } + } + + private func handleNotification(_ notification: ServerNotification) { + // TODO: Handle Notifications +// switch notification { +// case let .windowLogMessage(params): +// print("windowLogMessage \(params.type)\n```\n\(params.message)\n```\n") +// case let .windowShowMessage(params): +// print("windowShowMessage \(params.type)\n```\n\(params.message)\n```\n") +// case let .textDocumentPublishDiagnostics(params): +// print("textDocumentPublishDiagnostics: \(params)") +// case let .telemetryEvent(params): +// print("telemetryEvent: \(params)") +// case let .protocolCancelRequest(params): +// print("protocolCancelRequest: \(params)") +// case let .protocolProgress(params): +// print("protocolProgress: \(params)") +// case let .protocolLogTrace(params): +// print("protocolLogTrace: \(params)") +// } + } +} diff --git a/CodeEdit/Features/LSP/Service/LSPService.swift b/CodeEdit/Features/LSP/Service/LSPService.swift new file mode 100644 index 000000000..095696bbd --- /dev/null +++ b/CodeEdit/Features/LSP/Service/LSPService.swift @@ -0,0 +1,286 @@ +// +// LSPService.swift +// CodeEdit +// +// Created by Abe Malla on 2/7/24. +// + +import os.log +import JSONRPC +import Foundation +import LanguageClient +import LanguageServerProtocol +import CodeEditLanguages + +/// `LSPService` is a service class responsible for managing the lifecycle and event handling +/// of Language Server Protocol (LSP) clients within the CodeEdit application. It handles the initialization, +/// communication, and termination of language servers, ensuring that code assistance features +/// such as code completion, diagnostics, and more are available for various programming languages. +/// +/// This class uses Swift's concurrency model to manage background tasks and event streams +/// efficiently. Each language server runs in its own asynchronous task, listening for events and +/// handling them as they occur. The `LSPService` class also provides functionality to start +/// and stop individual language servers, as well as to stop all running servers. +/// +/// ## Example Usage +/// +/// ```swift +/// @Service var lspService +/// +/// try await lspService.startServer( +/// for: .python, +/// projectURL: projectURL, +/// workspaceFolders: workspaceFolders +/// ) +/// try await lspService.stopServer(for: .python) +/// ``` +/// +/// ## Completion Example +/// +/// ```swift +/// func testCompletion() async throws { +/// do { +/// guard var languageClient = self.languageClient(for: .python) else { +/// print("Failed to get client") +/// throw ServerManagerError.languageClientNotFound +/// } +/// +/// let testFilePathStr = "" +/// let testFileURL = URL(fileURLWithPath: testFilePathStr) +/// +/// // Tell server we opened a document +/// _ = await languageClient.addDocument(testFileURL) +/// +/// // Completion example +/// let textPosition = Position(line: 32, character: 18) // Lines and characters start at 0 +/// let completions = try await languageClient.requestCompletion( +/// document: testFileURL.absoluteString, +/// position: textPosition +/// ) +/// switch completions { +/// case .optionA(let completionItems): +/// // Handle the case where completions is an array of CompletionItem +/// print("\n*******\nCompletion Items:\n*******\n") +/// for item in completionItems { +/// let textEdits = LSPCompletionItemsUtil.getCompletionItemEdits( +/// startPosition: textPosition, +/// item: item +/// ) +/// for edit in textEdits { +/// print(edit) +/// } +/// } +/// +/// case .optionB(let completionList): +/// // Handle the case where completions is a CompletionList +/// print("\n*******\nCompletion Items:\n*******\n") +/// for item in completionList.items { +/// let textEdits = LSPCompletionItemsUtil.getCompletionItemEdits( +/// startPosition: textPosition, +/// item: item +/// ) +/// for edit in textEdits { +/// print(edit) +/// } +/// } +/// +/// print(completionList.items[0]) +/// +/// case .none: +/// print("No completions found") +/// } +/// +/// // Close the document +/// _ = await languageClient.closeDocument(testFilePathStr) +/// } catch { +/// print(error) +/// } +/// } +/// ``` +@MainActor +final class LSPService: ObservableObject { + let logger: Logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "", category: "LSPService") + + struct ClientKey: Hashable, Equatable { + let languageId: LanguageIdentifier + let workspacePath: String + + init(_ languageId: LanguageIdentifier, _ workspacePath: String) { + self.languageId = languageId + self.workspacePath = workspacePath + } + } + + /// Holds the active language clients + var languageClients: [ClientKey: LanguageServer] = [:] + /// Holds the language server configurations for all the installed language servers + var languageConfigs: [LanguageIdentifier: LanguageServerBinary] = [:] + /// Holds all the event listeners for each active language client + var eventListeningTasks: [ClientKey: Task] = [:] + + @AppSettings(\.developerSettings.lspBinaries) + var lspBinaries + + init() { + // Load the LSP binaries from the developer menu + for binary in lspBinaries { + if let language = LanguageIdentifier(rawValue: binary.key) { + self.languageConfigs[language] = LanguageServerBinary( + execPath: binary.value, + args: [], + env: ProcessInfo.processInfo.environment + ) + } + } + } + + /// Gets the language server for the specified language and workspace. + func server(for languageId: LanguageIdentifier, workspacePath: String) async -> InitializingServer? { + return languageClients[ClientKey(languageId, workspacePath)]?.lspInstance + } + + /// Gets the language client for the specified language + func languageClient(for languageId: LanguageIdentifier, workspacePath: String) -> LanguageServer? { + return languageClients[ClientKey(languageId, workspacePath)] + } + + /// Given a language and workspace path, will attempt to start the language server + /// - Parameters: + /// - languageId: The ID of the language server to start. + /// - workspacePath: The workspace this language server is being used in. + /// - Returns: The new language server. + func startServer( + for languageId: LanguageIdentifier, + workspacePath: String + ) async throws -> LanguageServer { + guard let serverBinary = languageConfigs[languageId] else { + logger.error("Couldn't find language sever binary for \(languageId.rawValue)") + throw LSPError.binaryNotFound + } + + logger.info("Starting \(languageId.rawValue) language server") + let server = try await LanguageServer.createServer( + for: languageId, + with: serverBinary, + workspacePath: workspacePath + ) + languageClients[ClientKey(languageId, workspacePath)] = server + logger.info("Successfully started \(languageId.rawValue) language server") + + self.startListeningToEvents(for: ClientKey(languageId, workspacePath)) + return server + } + + /// Notify all relevant language clients that a document was opened. + /// - Note: Must be invoked after the contents of the file are available. + /// - Parameter document: The code document that was opened. + func openDocument(_ document: CodeFileDocument) { + guard let workspace = document.findWorkspace(), + let workspacePath = workspace.fileURL?.absoluteURL.path(), + let lspLanguage = document.getLanguage().lspLanguage else { + return + } + Task.detached { + let languageServer: LanguageServer + do { + if let server = await self.languageClients[ClientKey(lspLanguage, workspacePath)] { + languageServer = server + } else { + languageServer = try await self.startServer(for: lspLanguage, workspacePath: workspacePath) + } + } catch { + // swiftlint:disable:next line_length + self.logger.error("Failed to find/start server for language: \(lspLanguage.rawValue), workspace: \(workspacePath, privacy: .private)") + return + } + do { + try await languageServer.openDocument(document) + } catch { + let uri = await document.languageServerURI + // swiftlint:disable:next line_length + self.logger.error("Failed to close document: \(uri ?? "", privacy: .private), language: \(lspLanguage.rawValue). Error \(error)") + } + } + } + + /// Notify all relevant language clients that a document was closed. + /// - Parameter document: The code document that was closed. + func closeDocument(_ document: CodeFileDocument) { + guard let workspace = document.findWorkspace(), + let workspacePath = workspace.fileURL?.absoluteURL.path(), + let lspLanguage = document.getLanguage().lspLanguage, + let languageClient = self.languageClient(for: lspLanguage, workspacePath: workspacePath), + let uri = document.languageServerURI else { + return + } + Task { + do { + try await languageClient.closeDocument(uri) + } catch { + // swiftlint:disable:next line_length + logger.error("Failed to close document: \(uri, privacy: .private), language: \(lspLanguage.rawValue). Error \(error)") + } + } + } + + /// Close all language clients for a workspace. + /// + /// This is intentionally synchronous so we can exit from the workspace document's ``WorkspaceDocument/close()`` + /// method ASAP. + /// + /// Errors thrown in this method are logged and otherwise not handled. + /// - Parameter workspacePath: The path of the workspace. + func closeWorkspace(_ workspacePath: String) { + Task { + let clientKeys = self.languageClients.filter({ $0.key.workspacePath == workspacePath }) + for (key, languageClient) in clientKeys { + do { + try await languageClient.shutdown() + } catch { + logger.error("Failed to shutdown \(key.languageId.rawValue) Language Server: Error \(error)") + } + } + for (key, _) in clientKeys { + self.languageClients.removeValue(forKey: key) + } + } + } + + /// Attempts to stop a running language server. Throws an error if the server is not found + /// or if the language server throws an error while trying to shutdown. + /// - Parameters: + /// - languageId: The ID of the language server to stop. + /// - workspacePath: The path of the workspace to stop the language server for. + func stopServer(forLanguage languageId: LanguageIdentifier, workspacePath: String) async throws { + guard let server = await self.server(for: languageId, workspacePath: workspacePath) else { + logger.error("Server not found for language \(languageId.rawValue) during stop operation") + throw ServerManagerError.serverNotFound + } + do { + try await server.shutdownAndExit() + } catch { + logger.error("Failed to stop server for language \(languageId.rawValue): \(error.localizedDescription)") + throw error + } + languageClients.removeValue(forKey: ClientKey(languageId, workspacePath)) + logger.info("Server stopped for language \(languageId.rawValue)") + + stopListeningToEvents(for: ClientKey(languageId, workspacePath)) + } + + /// Goes through all active language servers and attempts to shut them down. + func stopAllServers() async throws { + for key in languageClients.keys { + try await stopServer(forLanguage: key.languageId, workspacePath: key.workspacePath) + } + } +} + +// MARK: - Errors + +enum ServerManagerError: Error { + case serverNotFound + case serverStartFailed + case serverStopFailed + case languageClientNotFound +} diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorMenu.swift b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorMenu.swift index 9841859c1..01206e590 100644 --- a/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorMenu.swift +++ b/CodeEdit/Features/NavigatorArea/ProjectNavigator/OutlineView/ProjectNavigatorMenu.swift @@ -209,7 +209,13 @@ final class ProjectNavigatorMenu: NSMenu { @objc private func newFile() { guard let item else { return } - workspace?.workspaceFileManager?.addFile(fileName: "untitled", toFile: item) + do { + try workspace?.workspaceFileManager?.addFile(fileName: "untitled", toFile: item) + } catch { + let alert = NSAlert(error: error) + alert.addButton(withTitle: "Dismiss") + alert.runModal() + } outlineView.expandItem(item.isFolder ? item : item.parent) } diff --git a/CodeEdit/Features/NavigatorArea/ProjectNavigator/ProjectNavigatorToolbarBottom.swift b/CodeEdit/Features/NavigatorArea/ProjectNavigator/ProjectNavigatorToolbarBottom.swift index 5b58a21e7..d9d7bb2fd 100644 --- a/CodeEdit/Features/NavigatorArea/ProjectNavigator/ProjectNavigatorToolbarBottom.swift +++ b/CodeEdit/Features/NavigatorArea/ProjectNavigator/ProjectNavigatorToolbarBottom.swift @@ -102,7 +102,13 @@ struct ProjectNavigatorToolbarBottom: View { Button("Add File") { let filePathURL = activeTabURL() guard let rootFile = workspace.workspaceFileManager?.getFile(filePathURL.path) else { return } - workspace.workspaceFileManager?.addFile(fileName: "untitled", toFile: rootFile) + do { + try workspace.workspaceFileManager?.addFile(fileName: "untitled", toFile: rootFile) + } catch { + let alert = NSAlert(error: error) + alert.addButton(withTitle: "Dismiss") + alert.runModal() + } } Button("Add Folder") { let filePathURL = activeTabURL() diff --git a/CodeEdit/Utils/DependencyInjection/LazyServiceWrapper.swift b/CodeEdit/Utils/DependencyInjection/LazyServiceWrapper.swift new file mode 100644 index 000000000..b64dc7f61 --- /dev/null +++ b/CodeEdit/Utils/DependencyInjection/LazyServiceWrapper.swift @@ -0,0 +1,36 @@ +// +// LazyServiceWrapper.swift +// CodeEdit +// +// Created by Khan Winter on 9/9/24. +// + +/// A property wrapper that provides lazily-loaded access to a service instance. +/// +/// Using this wrapper, the service is only resolved when the property is first accessed. +@propertyWrapper +struct LazyService { + private let type: ServiceType + private var service: Service? + + init(_ type: ServiceType = .singleton) { + self.type = type + } + + var wrappedValue: Service { + mutating get { + if let service { + return service + } else { + guard let resolvedService = ServiceContainer.resolve(type, Service.self) else { + let serviceName = String(describing: Service.self) + fatalError("No service of type \(serviceName) registered!") + } + self.service = resolvedService + return resolvedService + } + } mutating set { + self.service = newValue + } + } +} diff --git a/CodeEdit/Features/OpenQuickly/ViewModels/URL+FuzzySearchable.swift b/CodeEdit/Utils/Extensions/URL/URL+FuzzySearchable.swift similarity index 100% rename from CodeEdit/Features/OpenQuickly/ViewModels/URL+FuzzySearchable.swift rename to CodeEdit/Utils/Extensions/URL/URL+FuzzySearchable.swift diff --git a/CodeEdit/Utils/Extensions/URL/URL+LanguageServer.swift b/CodeEdit/Utils/Extensions/URL/URL+LanguageServer.swift new file mode 100644 index 000000000..c6c86966a --- /dev/null +++ b/CodeEdit/Utils/Extensions/URL/URL+LanguageServer.swift @@ -0,0 +1,14 @@ +// +// URL+LanguageServer.swift +// CodeEdit +// +// Created by Khan Winter on 9/8/24. +// + +import Foundation + +extension URL { + var languageServerURI: String { + absoluteURL.path(percentEncoded: false) + } +} diff --git a/CodeEdit/Features/SourceControl/Accounts/Utils/URL+URLParameters.swift b/CodeEdit/Utils/Extensions/URL/URL+URLParameters.swift similarity index 100% rename from CodeEdit/Features/SourceControl/Accounts/Utils/URL+URLParameters.swift rename to CodeEdit/Utils/Extensions/URL/URL+URLParameters.swift diff --git a/CodeEditTests/Features/TerminalEmulator/ShellIntegrationTests.swift b/CodeEditTests/Features/ActivityViewer/TerminalEmulator/ShellIntegrationTests.swift similarity index 100% rename from CodeEditTests/Features/TerminalEmulator/ShellIntegrationTests.swift rename to CodeEditTests/Features/ActivityViewer/TerminalEmulator/ShellIntegrationTests.swift diff --git a/CodeEditTests/Features/LSP/BufferingServerConnection.swift b/CodeEditTests/Features/LSP/BufferingServerConnection.swift new file mode 100644 index 000000000..824e9853a --- /dev/null +++ b/CodeEditTests/Features/LSP/BufferingServerConnection.swift @@ -0,0 +1,46 @@ +// +// BufferingServerConnection.swift +// CodeEditTests +// +// Created by Khan Winter on 9/10/24. +// + +import Foundation +import LanguageClient +import LanguageServerProtocol +import JSONRPC + +class BufferingServerConnection: ServerConnection { + var eventSequence: EventSequence + private var id = 0 + + public var clientRequests: [ClientRequest] = [] + public var clientNotifications: [ClientNotification] = [] + + init() { + let (sequence, _) = EventSequence.makeStream() + self.eventSequence = sequence + } + + func sendNotification(_ notif: ClientNotification) async throws { + clientNotifications.append(notif) + } + + func sendRequest(_ request: ClientRequest) async throws -> Response { + clientRequests.append(request) + id += 1 + let response: Codable + switch request { + case .initialize: + var capabilities = ServerCapabilities() + capabilities.textDocumentSync = .optionA(.init( + openClose: true, change: .incremental, willSave: true, willSaveWaitUntil: false, save: .optionA(true) + )) + response = InitializationResponse(capabilities: .init(), serverInfo: nil) + default: + response = JSONRPCResponse(id: .numericId(0), result: JSONRPCErrors.internalError) + } + let data = try JSONEncoder().encode(response) + return try JSONDecoder().decode(Response.self, from: data) + } +} diff --git a/CodeEditTests/Features/LSP/LanguageServer+DocumentTests.swift b/CodeEditTests/Features/LSP/LanguageServer+DocumentTests.swift new file mode 100644 index 000000000..4b460405f --- /dev/null +++ b/CodeEditTests/Features/LSP/LanguageServer+DocumentTests.swift @@ -0,0 +1,145 @@ +// +// LanguageServer+DocumentTests.swift +// CodeEditTests +// +// Created by Khan Winter on 9/9/24. +// + +import XCTest +import LanguageClient +import LanguageServerProtocol + +@testable import CodeEdit + +final class LanguageServerDocumentTests: XCTestCase { + // Test opening documents in CodeEdit triggers creating a language server, + // further opened documents don't create new servers + + var tempTestDir: URL! + + override func setUp() { + do { + let tempDir = FileManager.default.temporaryDirectory.appending( + path: "codeedit-lsp-tests" + ) + try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true) + tempTestDir = tempDir + } catch { + XCTFail(error.localizedDescription) + } + } + + override func tearDown() { + do { + try FileManager.default.removeItem(at: tempTestDir) + } catch { + XCTFail(error.localizedDescription) + } + } + + func makeTestServer() async throws -> (connection: BufferingServerConnection, server: LanguageServer) { + let bufferingConnection = BufferingServerConnection() + var capabilities = ServerCapabilities() + capabilities.textDocumentSync = .optionA( + TextDocumentSyncOptions( + openClose: true, + change: .incremental, + willSave: true, + willSaveWaitUntil: false, + save: nil + ) + ) + let server = LanguageServer( + languageId: .swift, + binary: .init(execPath: "", args: [], env: nil), + lspInstance: InitializingServer( + server: bufferingConnection, + initializeParamsProvider: LanguageServer.getInitParams(workspacePath: tempTestDir.path()) + ), + serverCapabilities: capabilities, + rootPath: tempTestDir + ) + _ = try await server.lspInstance.initializeIfNeeded() + return (connection: bufferingConnection, server: server) + } + + func makeTestWorkspace() throws -> (WorkspaceDocument, CEWorkspaceFileManager) { + let workspace = WorkspaceDocument() + try workspace.read(from: tempTestDir, ofType: "") + guard let fileManager = workspace.workspaceFileManager else { + XCTFail("No File Manager") + fatalError("No File Manager") // never runs + } + return (workspace, fileManager) + } + + @MainActor + func testOpenFileInWorkspaceNotifiesLSP() async throws { + // Set up test server + let (connection, server) = try await makeTestServer() + + let lspService = ServiceContainer.resolve(.singleton, LSPService.self) + await MainActor.run { lspService?.languageClients[.init(.swift, tempTestDir.path() + "/")] = server } + + // Set up workspace + let (workspace, fileManager) = try makeTestWorkspace() + CodeEditDocumentController.shared.addDocument(workspace) + + // Add a CEWorkspaceFile + try fileManager.addFile(fileName: "example", toFile: fileManager.workspaceItem, useExtension: "swift") + guard let file = fileManager.childrenOfFile(fileManager.workspaceItem)?.first else { + XCTFail("No File") + return + } + + // Create a CodeFileDocument to test with, attach it to the workspace and file + let codeFile = try CodeFileDocument( + for: file.url, + withContentsOf: file.url, + ofType: "public.swift-source" + ) + file.fileDocument = codeFile + + // This should trigger a documentDidOpen event + CodeEditDocumentController.shared.addDocument(codeFile) + + let eventCountExpectation = expectation(description: "Pre-close event count") + // Wait off the main actor until we've received all the events + Task.detached { + while connection.clientNotifications.count + connection.clientRequests.count < 3 { + try await Task.sleep(for: .milliseconds(10)) + } + eventCountExpectation.fulfill() + } + + await fulfillment(of: [eventCountExpectation], timeout: 2) + + // This should then trigger a documentDidClose event + codeFile.close() + + let eventCloseExpectation = expectation(description: "Post-close event count") + Task.detached { + while connection.clientNotifications.count + connection.clientRequests.count < 4 { + try await Task.sleep(for: .milliseconds(10)) + } + eventCloseExpectation.fulfill() + } + await fulfillment(of: [eventCloseExpectation], timeout: 2) + + XCTAssertEqual( + connection.clientRequests.map { $0.method }, + [ + ClientRequest.Method.initialize, + ] + ) + + XCTAssertEqual( + connection.clientNotifications.map { $0.method }, + [ + ClientNotification.Method.initialized, + ClientNotification.Method.textDocumentDidOpen, + ClientNotification.Method.textDocumentDidClose + ] + ) + } +}