diff --git a/CodeEdit.xcodeproj/project.pbxproj b/CodeEdit.xcodeproj/project.pbxproj index cfd3741805..bf5c71f978 100644 --- a/CodeEdit.xcodeproj/project.pbxproj +++ b/CodeEdit.xcodeproj/project.pbxproj @@ -382,6 +382,8 @@ 6C2C155A29B4F4CC00EA60A5 /* Variadic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C2C155929B4F4CC00EA60A5 /* Variadic.swift */; }; 6C2C155D29B4F4E500EA60A5 /* SplitViewReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C2C155C29B4F4E500EA60A5 /* SplitViewReader.swift */; }; 6C2C156129B4F52F00EA60A5 /* SplitViewModifiers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C2C156029B4F52F00EA60A5 /* SplitViewModifiers.swift */; }; + 6C3B4CD12D0E2C2900C6759E /* SemanticTokenMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C3B4CD02D0E2C2900C6759E /* SemanticTokenMap.swift */; }; + 6C3B4CD42D0E2CB000C6759E /* SemanticTokenMapTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C3B4CD32D0E2CB000C6759E /* SemanticTokenMapTests.swift */; }; 6C3E12D32CC830D700DD12F1 /* RecentProjectsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C3E12D22CC830D700DD12F1 /* RecentProjectsStore.swift */; }; 6C3E12D62CC8388000DD12F1 /* URL+componentCompare.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C3E12D52CC8388000DD12F1 /* URL+componentCompare.swift */; }; 6C3E12D82CC83CB600DD12F1 /* RecentProjectsMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C3E12D72CC83CB600DD12F1 /* RecentProjectsMenu.swift */; }; @@ -440,6 +442,8 @@ 6C9619222C3F27F1009733CE /* Query.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C9619212C3F27F1009733CE /* Query.swift */; }; 6C9619242C3F2809009733CE /* ProjectPath.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C9619232C3F2809009733CE /* ProjectPath.swift */; }; 6C97EBCC2978760400302F95 /* AcknowledgementsWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C97EBCB2978760400302F95 /* AcknowledgementsWindowController.swift */; }; + 6C9AE66F2D148DD200FAE8D2 /* URL+FindWorkspace.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C9AE66E2D148DD200FAE8D2 /* URL+FindWorkspace.swift */; }; + 6C9AE6712D14A9F700FAE8D2 /* LazyServiceWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C9AE6702D14A9F700FAE8D2 /* LazyServiceWrapper.swift */; }; 6CA1AE952B46950000378EAB /* EditorInstance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CA1AE942B46950000378EAB /* EditorInstance.swift */; }; 6CAAF68A29BC9C2300A1F48A /* (null) in Sources */ = {isa = PBXBuildFile; }; 6CAAF69229BCC71C00A1F48A /* (null) in Sources */ = {isa = PBXBuildFile; }; @@ -458,16 +462,17 @@ 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 */; }; 6CC17B5B2C44258700834E2C /* WindowControllerPropertyWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CC17B5A2C44258700834E2C /* WindowControllerPropertyWrapper.swift */; }; + 6CC3D1FB2D1475EC00822B65 /* TextView+SemanticTokenRangeProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CC3D1FA2D1475EC00822B65 /* TextView+SemanticTokenRangeProvider.swift */; }; + 6CC3D1FD2D14761A00822B65 /* SemanticTokenMapRangeProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CC3D1FC2D14761A00822B65 /* SemanticTokenMapRangeProvider.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 */; }; + 6CD26C772C8EA83900ADBA38 /* URL+absolutePath.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CD26C762C8EA83900ADBA38 /* URL+absolutePath.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 */; }; - 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 */; }; @@ -1064,11 +1069,13 @@ 6C1CC99A2B1E7CBC0002349B /* FindNavigatorIndexBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FindNavigatorIndexBar.swift; sourceTree = ""; }; 6C1F3DA12C18C55800F6DEF6 /* ShellIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShellIntegrationTests.swift; sourceTree = ""; }; 6C23842E2C796B4C003FBDD4 /* GitChangedFileLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GitChangedFileLabel.swift; sourceTree = ""; }; - 6C278CC62C93971F0066F6D9 /* LSPContentCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LSPContentCoordinator.swift; sourceTree = ""; }; + 6C278CC62C93971F0066F6D9 /* LSPContentCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LSPContentCoordinator.swift; sourceTree = ""; wrapsLines = 0; }; 6C2C155729B4F49100EA60A5 /* SplitViewItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplitViewItem.swift; sourceTree = ""; }; 6C2C155929B4F4CC00EA60A5 /* Variadic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Variadic.swift; sourceTree = ""; }; 6C2C155C29B4F4E500EA60A5 /* SplitViewReader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplitViewReader.swift; sourceTree = ""; }; 6C2C156029B4F52F00EA60A5 /* SplitViewModifiers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplitViewModifiers.swift; sourceTree = ""; }; + 6C3B4CD02D0E2C2900C6759E /* SemanticTokenMap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SemanticTokenMap.swift; sourceTree = ""; }; + 6C3B4CD32D0E2CB000C6759E /* SemanticTokenMapTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SemanticTokenMapTests.swift; sourceTree = ""; }; 6C3E12D22CC830D700DD12F1 /* RecentProjectsStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecentProjectsStore.swift; sourceTree = ""; }; 6C3E12D52CC8388000DD12F1 /* URL+componentCompare.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+componentCompare.swift"; sourceTree = ""; }; 6C3E12D72CC83CB600DD12F1 /* RecentProjectsMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecentProjectsMenu.swift; sourceTree = ""; }; @@ -1120,6 +1127,8 @@ 6C9619232C3F2809009733CE /* ProjectPath.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProjectPath.swift; sourceTree = ""; }; 6C9619262C3F285C009733CE /* CodeEditTestPlan.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = CodeEditTestPlan.xctestplan; sourceTree = ""; }; 6C97EBCB2978760400302F95 /* AcknowledgementsWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AcknowledgementsWindowController.swift; sourceTree = ""; }; + 6C9AE66E2D148DD200FAE8D2 /* URL+FindWorkspace.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+FindWorkspace.swift"; sourceTree = ""; }; + 6C9AE6702D14A9F700FAE8D2 /* LazyServiceWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LazyServiceWrapper.swift; sourceTree = ""; }; 6CA1AE942B46950000378EAB /* EditorInstance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditorInstance.swift; sourceTree = ""; }; 6CABB1A029C5593800340467 /* SearchPanelView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchPanelView.swift; sourceTree = ""; }; 6CB52DC82AC8DC3E002E75B3 /* CEWorkspaceFileManager+FileManagement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CEWorkspaceFileManager+FileManagement.swift"; sourceTree = ""; }; @@ -1130,16 +1139,17 @@ 6CC17B502C43311900834E2C /* ProjectNavigatorViewController+NSOutlineViewDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProjectNavigatorViewController+NSOutlineViewDataSource.swift"; sourceTree = ""; }; 6CC17B522C43314000834E2C /* ProjectNavigatorViewController+NSOutlineViewDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProjectNavigatorViewController+NSOutlineViewDelegate.swift"; sourceTree = ""; }; 6CC17B5A2C44258700834E2C /* WindowControllerPropertyWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowControllerPropertyWrapper.swift; sourceTree = ""; }; + 6CC3D1FA2D1475EC00822B65 /* TextView+SemanticTokenRangeProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TextView+SemanticTokenRangeProvider.swift"; sourceTree = ""; }; + 6CC3D1FC2D14761A00822B65 /* SemanticTokenMapRangeProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SemanticTokenMapRangeProvider.swift; sourceTree = ""; }; 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 = ""; }; + 6CD26C762C8EA83900ADBA38 /* URL+absolutePath.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+absolutePath.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 = ""; }; @@ -1546,7 +1556,7 @@ 300051662BBD3A5D00A98562 /* ServiceContainer.swift */, 300051692BBD3A8200A98562 /* ServiceType.swift */, 3000516B2BBD3A9500A98562 /* ServiceWrapper.swift */, - 6CD26C862C8F90FD00ADBA38 /* LazyServiceWrapper.swift */, + 6C9AE6702D14A9F700FAE8D2 /* LazyServiceWrapper.swift */, ); path = DependencyInjection; sourceTree = ""; @@ -1587,6 +1597,7 @@ 30B087FB2C0D53080063A882 /* LSP */ = { isa = PBXGroup; children = ( + 6C3B4CD22D0E2C5400C6759E /* Editor */, 6CD26C732C8EA71F00ADBA38 /* LanguageServer */, 6CD26C742C8EA79100ADBA38 /* Service */, 30B087FA2C0D53080063A882 /* LSPUtil.swift */, @@ -2889,6 +2900,16 @@ path = ChangedFile; sourceTree = ""; }; + 6C3B4CD22D0E2C5400C6759E /* Editor */ = { + isa = PBXGroup; + children = ( + 6C278CC62C93971F0066F6D9 /* LSPContentCoordinator.swift */, + 6C3B4CD02D0E2C2900C6759E /* SemanticTokenMap.swift */, + 6CC3D1FC2D14761A00822B65 /* SemanticTokenMapRangeProvider.swift */, + ); + path = Editor; + sourceTree = ""; + }; 6C3E12D42CC830DE00DD12F1 /* Model */ = { isa = PBXGroup; children = ( @@ -3022,6 +3043,7 @@ isa = PBXGroup; children = ( 6CB94CFD2C9F1C9A00E8651C /* TextView+LSPRange.swift */, + 6CC3D1FA2D1475EC00822B65 /* TextView+SemanticTokenRangeProvider.swift */, ); path = TextView; sourceTree = ""; @@ -3072,7 +3094,6 @@ 6CD26C6D2C8EA1E600ADBA38 /* LanguageServerFileMap.swift */, 6CD26C782C8EA8A500ADBA38 /* LSPCache.swift */, 6CD26C792C8EA8A500ADBA38 /* LSPCache+Data.swift */, - 6C278CC62C93971F0066F6D9 /* LSPContentCoordinator.swift */, 30B0881E2C12626B0063A882 /* Capabilities */, ); path = LanguageServer; @@ -3091,8 +3112,9 @@ isa = PBXGroup; children = ( 613899BB2B6E709C00A5CAF6 /* URL+FuzzySearchable.swift */, - 6CD26C762C8EA83900ADBA38 /* URL+LanguageServer.swift */, + 6CD26C762C8EA83900ADBA38 /* URL+absolutePath.swift */, 587B9E2729301D8F00AC7927 /* URL+URLParameters.swift */, + 6C9AE66E2D148DD200FAE8D2 /* URL+FindWorkspace.swift */, ); path = URL; sourceTree = ""; @@ -3100,8 +3122,9 @@ 6CD26C882C8F91B600ADBA38 /* LSP */ = { isa = PBXGroup; children = ( - 6CD26C892C8F91ED00ADBA38 /* LanguageServer+DocumentTests.swift */, 6C7D6D452C9092EC00B69EE0 /* BufferingServerConnection.swift */, + 6CD26C892C8F91ED00ADBA38 /* LanguageServer+DocumentTests.swift */, + 6C3B4CD32D0E2CB000C6759E /* SemanticTokenMapTests.swift */, ); path = LSP; sourceTree = ""; @@ -4122,6 +4145,7 @@ 201169E72837B5CA00F92B46 /* SourceControlManager.swift in Sources */, 58822528292C280D00E83CDE /* StatusBarEncodingSelector.swift in Sources */, 0FD96BCE2BEF42530025A697 /* CodeEditWindowController+Toolbar.swift in Sources */, + 6CC3D1FB2D1475EC00822B65 /* TextView+SemanticTokenRangeProvider.swift in Sources */, 6C7F37FE2A3EA6FA00217B83 /* View+focusedValue.swift in Sources */, 6653EE552C34817900B82DE2 /* QuickSearchResultLabel.swift in Sources */, 6139B9152C29B36100CA584B /* CEActiveTask.swift in Sources */, @@ -4291,6 +4315,7 @@ 6C092EE02A53BFCF00489202 /* WorkspaceStateKey.swift in Sources */, 618725A82C29F05500987354 /* OptionMenuItemView.swift in Sources */, 613899B52B6E700300A5CAF6 /* FuzzySearchModels.swift in Sources */, + 6C9AE66F2D148DD200FAE8D2 /* URL+FindWorkspace.swift in Sources */, 58D01C94293167DC00C5B6B4 /* Color+HEX.swift in Sources */, 6C578D8729CD345900DC73B2 /* ExtensionSceneView.swift in Sources */, 617DB3D02C25AFAE00B58BFE /* TaskNotificationHandler.swift in Sources */, @@ -4339,13 +4364,13 @@ 6C6BD6EF29CD12E900235D17 /* ExtensionManagerWindow.swift in Sources */, 30B087FF2C0D53080063A882 /* LanguageServer+Completion.swift in Sources */, 61C7E82F2C6CDBA500845336 /* Theme+FuzzySearchable.swift in Sources */, + 6C3B4CD12D0E2C2900C6759E /* SemanticTokenMap.swift in Sources */, 6CFF967629BEBCD900182D6F /* FileCommands.swift in Sources */, B60718462B17DC15009CDAB4 /* RepoOutlineGroupItem.swift in Sources */, 613899B32B6E6FEE00A5CAF6 /* FuzzySearchable.swift in Sources */, 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 */, @@ -4418,7 +4443,7 @@ 6C147C4129A328BF0089B630 /* EditorLayout.swift in Sources */, B6D7EA592971078500301FAC /* InspectorSection.swift in Sources */, B6AB09A32AAABFEC0003A3A6 /* EditorTabBarLeadingAccessories.swift in Sources */, - 6CD26C772C8EA83900ADBA38 /* URL+LanguageServer.swift in Sources */, + 6CD26C772C8EA83900ADBA38 /* URL+absolutePath.swift in Sources */, B69BFDC72B0686910050D9A6 /* GitClient+Initiate.swift in Sources */, 58F2EAEF292FB2B0004A9BDE /* ThemeSettingsView.swift in Sources */, 85745D632A38F8D900089AAB /* String+HighlightOccurrences.swift in Sources */, @@ -4466,6 +4491,7 @@ 611191FA2B08CC9000D4459B /* SearchIndexer.swift in Sources */, 58822532292C280D00E83CDE /* UtilityAreaViewModel.swift in Sources */, 043BCF03281DA18A000AC47C /* WorkspaceDocument+SearchState.swift in Sources */, + 6CC3D1FD2D14761A00822B65 /* SemanticTokenMapRangeProvider.swift in Sources */, 58822527292C280D00E83CDE /* StatusBarIndentSelector.swift in Sources */, 587B9E9629301D8F00AC7927 /* BitBucketRepositories.swift in Sources */, 5878DAA6291AE76700DD95A3 /* OpenQuicklyPreviewView.swift in Sources */, @@ -4506,6 +4532,7 @@ B6AB09B32AB919CF0003A3A6 /* View+actionBar.swift in Sources */, 617DB3DA2C25B07F00B58BFE /* TaskNotificationsDetailView.swift in Sources */, 6C05A8AF284D0CA3007F4EAA /* WorkspaceDocument+Listeners.swift in Sources */, + 6C9AE6712D14A9F700FAE8D2 /* LazyServiceWrapper.swift in Sources */, 588847632992A2A200996D95 /* CEWorkspaceFile.swift in Sources */, 30B088082C0D53080063A882 /* LanguageServer+FoldingRange.swift in Sources */, 6C2C155D29B4F4E500EA60A5 /* SplitViewReader.swift in Sources */, @@ -4539,6 +4566,7 @@ 6195E3112B640485007261CA /* WorkspaceDocument+SearchState+IndexTests.swift in Sources */, 6130536B2B24722C00D767E3 /* AsyncIndexingTests.swift in Sources */, 613899C02B6E70FE00A5CAF6 /* FuzzySearchTests.swift in Sources */, + 6C3B4CD42D0E2CB000C6759E /* SemanticTokenMapTests.swift in Sources */, 6195E30D2B64044F007261CA /* WorkspaceDocument+SearchState+FindTests.swift in Sources */, 6195E30F2B640474007261CA /* WorkspaceDocument+SearchState+FindAndReplaceTests.swift in Sources */, 587B612E293419B700D5CD8F /* CodeFileTests.swift in Sources */, @@ -5696,7 +5724,7 @@ repositoryURL = "https://github.com/CodeEditApp/CodeEditSourceEditor"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 0.9.0; + minimumVersion = 0.9.1; }; }; 6C0617D42BDB4432008C9C42 /* XCRemoteSwiftPackageReference "LogStream" */ = { diff --git a/CodeEdit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/CodeEdit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index ec048d540d..b65c217afc 100644 --- a/CodeEdit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/CodeEdit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -33,8 +33,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/CodeEditApp/CodeEditSourceEditor", "state" : { - "revision" : "bfcde1fc536e4159ca3d596fa5b8bbbeb1524362", - "version" : "0.9.0" + "revision" : "b0688fa59fb8060840fb013afb4d6e6a96000f14", + "version" : "0.9.1" } }, { diff --git a/CodeEdit/AppDelegate.swift b/CodeEdit/AppDelegate.swift index 6c77d134b3..7945d1e715 100644 --- a/CodeEdit/AppDelegate.swift +++ b/CodeEdit/AppDelegate.swift @@ -20,7 +20,6 @@ final class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject { @LazyService var lspService: LSPService func applicationDidFinishLaunching(_ notification: Notification) { - setupServiceContainer() enableWindowSizeSaveOnQuit() Settings.shared.preferences.general.appAppearance.applyAppearance() checkForFilesToOpen() @@ -271,14 +270,6 @@ final class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject { workspace.taskManager?.stopAllTasks() } } - - /// 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/CodeEditApp.swift b/CodeEdit/CodeEditApp.swift index dffcd0ea75..51dd6c2365 100644 --- a/CodeEdit/CodeEditApp.swift +++ b/CodeEdit/CodeEditApp.swift @@ -15,6 +15,11 @@ struct CodeEditApp: App { let updater: SoftwareUpdater = SoftwareUpdater() init() { + // Register singleton services before anything else + ServiceContainer.register( + LSPService() + ) + _ = CodeEditDocumentController.shared NSMenuItem.swizzle() NSSplitViewItem.swizzle() diff --git a/CodeEdit/Features/Documents/CodeFileDocument/CodeFileDocument.swift b/CodeEdit/Features/Documents/CodeFileDocument/CodeFileDocument.swift index e3afda72c1..4e1638b9d6 100644 --- a/CodeEdit/Features/Documents/CodeFileDocument/CodeFileDocument.swift +++ b/CodeEdit/Features/Documents/CodeFileDocument/CodeFileDocument.swift @@ -29,7 +29,10 @@ final class CodeFileDocument: NSDocument, ObservableObject { static let logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "", category: "CodeFileDocument") - @Service var lspService: LSPService + /// Sent when the document is opened. The document will be sent in the notification's object. + static let didOpenNotification = Notification.Name(rawValue: "CodeFileDocument.didOpen") + /// Sent when the document is closed. The document's `fileURL` will be sent in the notification's object. + static let didCloseNotification = Notification.Name(rawValue: "CodeFileDocument.didClose") /// The text content of the document, stored as a text storage /// @@ -47,11 +50,8 @@ final class CodeFileDocument: NSDocument, ObservableObject { /// See ``CodeEditSourceEditor/CombineCoordinator``. @Published var contentCoordinator: CombineCoordinator = CombineCoordinator() - lazy var languageServerCoordinator: LSPContentCoordinator = { - let coordinator = LSPContentCoordinator() - coordinator.uri = self.languageServerURI - return coordinator - }() + /// Set by ``LanguageServer`` when initialized. + @Published var lspCoordinator: LSPContentCoordinator? /// Used to override detected languages. @Published var language: CodeLanguage? @@ -84,7 +84,7 @@ final class CodeFileDocument: NSDocument, ObservableObject { } /// A stable string to use when identifying documents with language servers. - var languageServerURI: String? { fileURL?.languageServerURI } + var languageServerURI: String? { fileURL?.absolutePath } /// Specify options for opening the file such as the initial cursor positions. /// Nulled by ``CodeFileView`` on first load. @@ -161,6 +161,7 @@ final class CodeFileDocument: NSDocument, ObservableObject { } else { Self.logger.error("Failed to read file from data using encoding: \(rawEncoding)") } + NotificationCenter.default.post(name: Self.didOpenNotification, object: self) } /// Triggered when change occurred @@ -187,7 +188,7 @@ final class CodeFileDocument: NSDocument, ObservableObject { override func close() { super.close() - lspService.closeDocument(self) + NotificationCenter.default.post(name: Self.didCloseNotification, object: fileURL) } func getLanguage() -> CodeLanguage { @@ -202,15 +203,6 @@ final class CodeFileDocument: NSDocument, ObservableObject { } 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 + fileURL?.findWorkspace() } } diff --git a/CodeEdit/Features/Documents/Controllers/CodeEditDocumentController.swift b/CodeEdit/Features/Documents/Controllers/CodeEditDocumentController.swift index 778f707345..034d3de75e 100644 --- a/CodeEdit/Features/Documents/Controllers/CodeEditDocumentController.swift +++ b/CodeEdit/Features/Documents/Controllers/CodeEditDocumentController.swift @@ -12,7 +12,7 @@ final class CodeEditDocumentController: NSDocumentController { @Environment(\.openWindow) private var openWindow - @LazyService var lspService: LSPService + @Service var lspService: LSPService private let fileManager = FileManager.default @@ -92,13 +92,6 @@ final class CodeEditDocumentController: NSDocumentController { } } } - - override func addDocument(_ document: NSDocument) { - super.addDocument(document) - if let document = document as? CodeFileDocument { - lspService.openDocument(document) - } - } } extension NSDocumentController { diff --git a/CodeEdit/Features/Editor/Views/CodeFileView.swift b/CodeEdit/Features/Editor/Views/CodeFileView.swift index 7f80a16ceb..ba1fbe6603 100644 --- a/CodeEdit/Features/Editor/Views/CodeFileView.swift +++ b/CodeEdit/Features/Editor/Views/CodeFileView.swift @@ -56,10 +56,9 @@ struct CodeFileView: View { init(codeFile: CodeFileDocument, textViewCoordinators: [TextViewCoordinator] = [], isEditable: Bool = true) { self._codeFile = .init(wrappedValue: codeFile) - self.textViewCoordinators = textViewCoordinators + [ - codeFile.contentCoordinator, - codeFile.languageServerCoordinator - ] + self.textViewCoordinators = textViewCoordinators + + [codeFile.contentCoordinator] + + [codeFile.lspCoordinator].compactMap({ $0 }) self.isEditable = isEditable if let openOptions = codeFile.openOptions { @@ -138,7 +137,6 @@ struct CodeFileView: View { undoManager: undoManager, coordinators: textViewCoordinators ) - .id(codeFile.fileURL) .background { if colorScheme == .dark { diff --git a/CodeEdit/Features/LSP/LanguageServer/LSPContentCoordinator.swift b/CodeEdit/Features/LSP/Editor/LSPContentCoordinator.swift similarity index 88% rename from CodeEdit/Features/LSP/LanguageServer/LSPContentCoordinator.swift rename to CodeEdit/Features/LSP/Editor/LSPContentCoordinator.swift index 9c405f64ee..dc17481e61 100644 --- a/CodeEdit/Features/LSP/LanguageServer/LSPContentCoordinator.swift +++ b/CodeEdit/Features/LSP/Editor/LSPContentCoordinator.swift @@ -33,9 +33,12 @@ class LSPContentCoordinator: TextViewCoordinator, TextViewDelegate { private var task: Task? weak var languageServer: LanguageServer? - var uri: String? + var documentURI: String - init() { + /// Initializes a content coordinator, and begins an async stream of updates + init(documentURI: String, languageServer: LanguageServer) { + self.documentURI = documentURI + self.languageServer = languageServer self.stream = AsyncStream { continuation in self.sequenceContinuation = continuation } @@ -71,12 +74,11 @@ class LSPContentCoordinator: TextViewCoordinator, TextViewDelegate { } func textView(_ textView: TextView, didReplaceContentsIn range: NSRange, with string: String) { - guard let uri, - let lspRange = editedRange else { + guard let lspRange = editedRange else { return } self.editedRange = nil - self.sequenceContinuation?.yield(SequenceElement(uri: uri, range: lspRange, string: string)) + self.sequenceContinuation?.yield(SequenceElement(uri: documentURI, range: lspRange, string: string)) } func destroy() { diff --git a/CodeEdit/Features/LSP/Editor/SemanticTokenMap.swift b/CodeEdit/Features/LSP/Editor/SemanticTokenMap.swift new file mode 100644 index 0000000000..5a196cf60f --- /dev/null +++ b/CodeEdit/Features/LSP/Editor/SemanticTokenMap.swift @@ -0,0 +1,90 @@ +// +// SemanticTokenMap.swift +// CodeEdit +// +// Created by Khan Winter on 11/10/24. +// + +import LanguageClient +import LanguageServerProtocol +import CodeEditSourceEditor +import CodeEditTextView + +// swiftlint:disable line_length +/// Creates a mapping from a language server's semantic token options to a format readable by CodeEdit. +/// Provides a convenience method for mapping tokens received from the server to highlight ranges suitable for +/// highlighting in the editor. +/// +/// Use this type to handle the initially received semantic highlight capabilities structures. This type will figure +/// out how to read it into a format it can use. +/// +/// After initialization, the map is static until the server is reinitialized. Consequently, this type is `Sendable` +/// and immutable after initialization. +/// +/// This type is not coupled to any text system via the use of the ``SemanticTokenMapRangeProvider``. When decoding to +/// highlight ranges, provide a type that can provide ranges for highlighting. +/// +/// [LSP Spec](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#semanticTokensLegend) +struct SemanticTokenMap: Sendable { // swiftlint:enable line_length + private let tokenTypeMap: [CaptureName?] + private let modifierMap: [CaptureModifier?] + + init(semanticCapability: TwoTypeOption) { + let legend: SemanticTokensLegend + switch semanticCapability { + case .optionA(let tokensOptions): + legend = tokensOptions.legend + case .optionB(let tokensRegistrationOptions): + legend = tokensRegistrationOptions.legend + } + + tokenTypeMap = legend.tokenTypes.map { CaptureName.fromString($0) } + modifierMap = legend.tokenModifiers.map { CaptureModifier.fromString($0) } + } + + /// Decodes the compressed semantic token data into a `HighlightRange` type for use in an editor. + /// This is marked main actor to prevent runtime errors, due to the use of the actor-isolated `rangeProvider`. + /// - Parameters: + /// - tokens: Semantic tokens from a language server. + /// - rangeProvider: The provider to use to translate token ranges to text view ranges. + /// - Returns: An array of decoded highlight ranges. + @MainActor + func decode(tokens: SemanticTokens, using rangeProvider: SemanticTokenMapRangeProvider) -> [HighlightRange] { + tokens.decode().compactMap { token in + guard let range = rangeProvider.nsRangeFrom(line: token.line, char: token.char, length: token.length) else { + return nil + } + + let modifiers = decodeModifier(token.modifiers) + + // Capture types are indicated by the index of the set bit. + let type = token.type > 0 ? Int(token.type.trailingZeroBitCount) : -1 // Don't try to decode 0 + let capture = tokenTypeMap.indices.contains(type) ? tokenTypeMap[type] : nil + + return HighlightRange( + range: range, + capture: capture, + modifiers: modifiers + ) + } + } + + /// Decodes a raw modifier value into a set of capture modifiers. + /// - Parameter raw: The raw modifier integer to decode. + /// - Returns: A set of modifiers for highlighting. + func decodeModifier(_ raw: UInt32) -> CaptureModifierSet { + var modifiers: CaptureModifierSet = [] + var raw = raw + while raw > 0 { + let idx = raw.trailingZeroBitCount + raw &= ~(1 << idx) + // We don't use `[safe:]` because it creates a double optional here. If someone knows how to extend + // a collection of optionals to make that return only a single optional this could be updated. + guard let modifier = modifierMap.indices.contains(idx) ? modifierMap[idx] : nil else { + continue + } + modifiers.insert(modifier) + } + return modifiers + } +} diff --git a/CodeEdit/Features/LSP/Editor/SemanticTokenMapRangeProvider.swift b/CodeEdit/Features/LSP/Editor/SemanticTokenMapRangeProvider.swift new file mode 100644 index 0000000000..c8ab4a40cb --- /dev/null +++ b/CodeEdit/Features/LSP/Editor/SemanticTokenMapRangeProvider.swift @@ -0,0 +1,13 @@ +// +// SemanticTokenMapRangeProvider.swift +// CodeEdit +// +// Created by Khan Winter on 12/19/24. +// + +import Foundation + +@MainActor +protocol SemanticTokenMapRangeProvider { + func nsRangeFrom(line: UInt32, char: UInt32, length: UInt32) -> NSRange? +} diff --git a/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+DocumentSync.swift b/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+DocumentSync.swift index dcf12fa834..563604aa7e 100644 --- a/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+DocumentSync.swift +++ b/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+DocumentSync.swift @@ -19,7 +19,7 @@ extension LanguageServer { } logger.debug("Opening Document \(content.uri, privacy: .private)") - self.openFiles.addDocument(document) + openFiles.addDocument(document, for: self) let textDocument = TextDocumentItem( uri: content.uri, @@ -28,7 +28,8 @@ extension LanguageServer { text: content.string ) try await lspInstance.textDocumentDidOpen(DidOpenTextDocumentParams(textDocument: textDocument)) - await updateIsolatedTextCoordinator(for: document) + + await updateIsolatedDocument(document, coordinator: openFiles.contentCoordinator(for: document)) } catch { logger.warning("addDocument: Error \(error)") throw error @@ -118,10 +119,9 @@ extension LanguageServer { return DocumentContent(uri: uri, language: language, string: content) } - /// Updates the actor-isolated document's text coordinator to map to this server. @MainActor - fileprivate func updateIsolatedTextCoordinator(for document: CodeFileDocument) { - document.languageServerCoordinator.languageServer = self + private func updateIsolatedDocument(_ document: CodeFileDocument, coordinator: LSPContentCoordinator?) { + document.lspCoordinator = coordinator } // swiftlint:disable line_length diff --git a/CodeEdit/Features/LSP/LanguageServer/LanguageServer.swift b/CodeEdit/Features/LSP/LanguageServer/LanguageServer.swift index 9f00b4f4c3..eab8be5504 100644 --- a/CodeEdit/Features/LSP/LanguageServer/LanguageServer.swift +++ b/CodeEdit/Features/LSP/LanguageServer/LanguageServer.swift @@ -22,8 +22,14 @@ class LanguageServer { /// A cache to hold responses from the server, to minimize duplicate server requests let lspCache = LSPCache() + /// Tracks documents and their associated objects. + /// Use this property when adding new objects that need to track file data, or have a state associated with the + /// language server and a document. For example, the content coordinator. let openFiles: LanguageServerFileMap + /// Maps the language server's highlight config to one CodeEdit can read. See ``SemanticTokenMap``. + let highlightMap: SemanticTokenMap? + /// The configuration options this server supports. var serverCapabilities: ServerCapabilities @@ -49,6 +55,11 @@ class LanguageServer { subsystem: Bundle.main.bundleIdentifier ?? "", category: "LanguageServer.\(languageId.rawValue)" ) + if let semanticTokensProvider = serverCapabilities.semanticTokensProvider { + self.highlightMap = SemanticTokenMap(semanticCapability: semanticTokensProvider) + } else { + self.highlightMap = nil // Server doesn't support semantic highlights + } } /// Creates and initializes a language server. @@ -82,6 +93,8 @@ class LanguageServer { ) } + // MARK: - Make Local Server Connection + /// Creates a data channel for sending and receiving data with an LSP. /// - Parameters: /// - languageId: The ID of the language to create the channel for. @@ -105,6 +118,8 @@ class LanguageServer { } } + // MARK: - Get Init Params + // swiftlint:disable function_body_length static func getInitParams(workspacePath: String) -> InitializingServer.InitializeParamsProvider { let provider: InitializingServer.InitializeParamsProvider = { @@ -136,15 +151,15 @@ class LanguageServer { // 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: [], + dynamicRegistration: false, + requests: .init(range: true, delta: true), + tokenTypes: SemanticTokenTypes.allStrings, + tokenModifiers: SemanticTokenModifiers.allStrings, formats: [.relative], overlappingTokenSupport: true, multilineTokenSupport: true, serverCancelSupport: true, - augmentsSyntaxTokens: false + augmentsSyntaxTokens: true ) ) diff --git a/CodeEdit/Features/LSP/LanguageServer/LanguageServerFileMap.swift b/CodeEdit/Features/LSP/LanguageServer/LanguageServerFileMap.swift index 0f3d4469f7..c681e894a6 100644 --- a/CodeEdit/Features/LSP/LanguageServer/LanguageServerFileMap.swift +++ b/CodeEdit/Features/LSP/LanguageServer/LanguageServerFileMap.swift @@ -8,9 +8,17 @@ import Foundation import LanguageServerProtocol +/// Tracks data associated with files and language servers. class LanguageServerFileMap { + /// Extend this struct as more objects are associated with a code document. + private struct DocumentObject { + let uri: String + var documentVersion: Int + var contentCoordinator: LSPContentCoordinator + } + private var trackedDocuments: NSMapTable - private var trackedDocumentVersions: [String: Int] = [:] + private var trackedDocumentData: [String: DocumentObject] = [:] init() { trackedDocuments = NSMapTable(valueOptions: [.weakMemory]) @@ -18,15 +26,19 @@ class LanguageServerFileMap { // MARK: - Track & Remove Documents - func addDocument(_ document: CodeFileDocument) { + func addDocument(_ document: CodeFileDocument, for server: LanguageServer) { guard let uri = document.languageServerURI else { return } trackedDocuments.setObject(document, forKey: uri as NSString) - trackedDocumentVersions[uri] = 0 + trackedDocumentData[uri] = DocumentObject( + uri: uri, + documentVersion: 0, + contentCoordinator: LSPContentCoordinator(documentURI: uri, languageServer: server) + ) } func document(for uri: DocumentUri) -> CodeFileDocument? { let url = URL(filePath: uri) - return trackedDocuments.object(forKey: url.languageServerURI as NSString) + return trackedDocuments.object(forKey: url.absolutePath as NSString) } func removeDocument(for document: CodeFileDocument) { @@ -36,7 +48,7 @@ class LanguageServerFileMap { func removeDocument(for uri: DocumentUri) { trackedDocuments.removeObject(forKey: uri as NSString) - trackedDocumentVersions.removeValue(forKey: uri) + trackedDocumentData.removeValue(forKey: uri) } // MARK: - Version Number Tracking @@ -47,8 +59,8 @@ class LanguageServerFileMap { } func incrementVersion(for uri: DocumentUri) -> Int { - trackedDocumentVersions[uri] = (trackedDocumentVersions[uri] ?? 0) + 1 - return trackedDocumentVersions[uri] ?? 0 + trackedDocumentData[uri]?.documentVersion += 1 + return trackedDocumentData[uri]?.documentVersion ?? 0 } func documentVersion(for document: CodeFileDocument) -> Int? { @@ -57,6 +69,17 @@ class LanguageServerFileMap { } func documentVersion(for uri: DocumentUri) -> Int? { - return trackedDocumentVersions[uri] + return trackedDocumentData[uri]?.documentVersion + } + + // MARK: - Content Coordinator + + func contentCoordinator(for document: CodeFileDocument) -> LSPContentCoordinator? { + guard let uri = document.languageServerURI else { return nil } + return contentCoordinator(for: uri) + } + + func contentCoordinator(for uri: DocumentUri) -> LSPContentCoordinator? { + trackedDocumentData[uri]?.contentCoordinator } } diff --git a/CodeEdit/Features/LSP/Service/LSPService.swift b/CodeEdit/Features/LSP/Service/LSPService.swift index 0c4b5a812c..2eaab98d69 100644 --- a/CodeEdit/Features/LSP/Service/LSPService.swift +++ b/CodeEdit/Features/LSP/Service/LSPService.swift @@ -132,10 +132,32 @@ final class LSPService: ObservableObject { ) } } + + NotificationCenter.default.addObserver( + forName: CodeFileDocument.didOpenNotification, + object: nil, + queue: .main + ) { notification in + MainActor.assumeIsolated { + guard let document = notification.object as? CodeFileDocument else { return } + self.openDocument(document) + } + } + + NotificationCenter.default.addObserver( + forName: CodeFileDocument.didCloseNotification, + object: nil, + queue: .main + ) { notification in + MainActor.assumeIsolated { + guard let url = notification.object as? URL else { return } + self.closeDocument(url) + } + } } /// Gets the language server for the specified language and workspace. - func server(for languageId: LanguageIdentifier, workspacePath: String) async -> InitializingServer? { + func server(for languageId: LanguageIdentifier, workspacePath: String) -> InitializingServer? { return languageClients[ClientKey(languageId, workspacePath)]?.lspInstance } @@ -180,10 +202,10 @@ final class LSPService: ObservableObject { let lspLanguage = document.getLanguage().lspLanguage else { return } - Task.detached { + Task { let languageServer: LanguageServer do { - if let server = await self.languageClients[ClientKey(lspLanguage, workspacePath)] { + if let server = self.languageClients[ClientKey(lspLanguage, workspacePath)] { languageServer = server } else { languageServer = try await self.startServer(for: lspLanguage, workspacePath: workspacePath) @@ -204,21 +226,19 @@ final class LSPService: ObservableObject { } /// 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 { + /// - Parameter url: The url of the document that was closed + func closeDocument(_ url: URL) { + guard let languageClient = languageClients.first(where: { + $0.value.openFiles.document(for: url.absolutePath) != nil + })?.value else { return } Task { do { - try await languageClient.closeDocument(uri) + try await languageClient.closeDocument(url.absolutePath) } catch { // swiftlint:disable:next line_length - logger.error("Failed to close document: \(uri, privacy: .private), language: \(lspLanguage.rawValue). Error \(error)") + logger.error("Failed to close document: \(url.absolutePath, privacy: .private), language: \(languageClient.languageId.rawValue). Error \(error)") } } } @@ -252,7 +272,7 @@ final class LSPService: ObservableObject { /// - 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 { + guard let server = server(for: languageId, workspacePath: workspacePath) else { logger.error("Server not found for language \(languageId.rawValue) during stop operation") throw ServerManagerError.serverNotFound } diff --git a/CodeEdit/Utils/Extensions/TextView/TextView+SemanticTokenRangeProvider.swift b/CodeEdit/Utils/Extensions/TextView/TextView+SemanticTokenRangeProvider.swift new file mode 100644 index 0000000000..f41060423e --- /dev/null +++ b/CodeEdit/Utils/Extensions/TextView/TextView+SemanticTokenRangeProvider.swift @@ -0,0 +1,18 @@ +// +// TextView+SemanticTokenRangeProvider.swift +// CodeEdit +// +// Created by Khan Winter on 12/19/24. +// + +import Foundation +import CodeEditTextView + +extension TextView: SemanticTokenMapRangeProvider { + func nsRangeFrom(line: UInt32, char: UInt32, length: UInt32) -> NSRange? { + guard let line = layoutManager.textLineForIndex(Int(line)) else { + return nil + } + return NSRange(location: line.range.location + Int(char), length: Int(length)) + } +} diff --git a/CodeEdit/Utils/Extensions/URL/URL+FindWorkspace.swift b/CodeEdit/Utils/Extensions/URL/URL+FindWorkspace.swift new file mode 100644 index 0000000000..a3259ef6bd --- /dev/null +++ b/CodeEdit/Utils/Extensions/URL/URL+FindWorkspace.swift @@ -0,0 +1,20 @@ +// +// URL+FindWorkspace.swift +// CodeEdit +// +// Created by Khan Winter on 12/19/24. +// + +import Foundation + +extension URL { + /// Finds a workspace that contains the url. + func findWorkspace() -> WorkspaceDocument? { + CodeEditDocumentController.shared.documents.first(where: { doc in + guard let workspace = doc as? WorkspaceDocument 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(absolutePath, createIfNotFound: true) != nil + }) as? WorkspaceDocument + } +} diff --git a/CodeEdit/Utils/Extensions/URL/URL+LanguageServer.swift b/CodeEdit/Utils/Extensions/URL/URL+absolutePath.swift similarity index 83% rename from CodeEdit/Utils/Extensions/URL/URL+LanguageServer.swift rename to CodeEdit/Utils/Extensions/URL/URL+absolutePath.swift index c6c86966a2..8270af913f 100644 --- a/CodeEdit/Utils/Extensions/URL/URL+LanguageServer.swift +++ b/CodeEdit/Utils/Extensions/URL/URL+absolutePath.swift @@ -8,7 +8,7 @@ import Foundation extension URL { - var languageServerURI: String { + var absolutePath: String { absoluteURL.path(percentEncoded: false) } } diff --git a/CodeEditTests/Features/LSP/BufferingServerConnection.swift b/CodeEditTests/Features/LSP/BufferingServerConnection.swift index 824e9853a4..4fabf0a4fc 100644 --- a/CodeEditTests/Features/LSP/BufferingServerConnection.swift +++ b/CodeEditTests/Features/LSP/BufferingServerConnection.swift @@ -10,8 +10,20 @@ import LanguageClient import LanguageServerProtocol import JSONRPC +/// Mock server connection that retains all requests and notifications in an array for comparing later. +/// +/// To listen for changes, this type produces an async stream of all requests and notifications. Use the +/// `clientEventSequence` sequence to receive a copy of both whenever they're updated. +/// class BufferingServerConnection: ServerConnection { - var eventSequence: EventSequence + typealias ClientEventSequence = AsyncStream<([ClientRequest], [ClientNotification])> + + public var eventSequence: EventSequence + + /// A sequence of all events. + public var clientEventSequence: ClientEventSequence + private var clientEventContinuation: ClientEventSequence.Continuation + private var id = 0 public var clientRequests: [ClientRequest] = [] @@ -20,13 +32,19 @@ class BufferingServerConnection: ServerConnection { init() { let (sequence, _) = EventSequence.makeStream() self.eventSequence = sequence + (clientEventSequence, clientEventContinuation) = ClientEventSequence.makeStream() } func sendNotification(_ notif: ClientNotification) async throws { clientNotifications.append(notif) + clientEventContinuation.yield((clientRequests, clientNotifications)) } func sendRequest(_ request: ClientRequest) async throws -> Response { + defer { + clientEventContinuation.yield((clientRequests, clientNotifications)) + } + clientRequests.append(request) id += 1 let response: Codable diff --git a/CodeEditTests/Features/LSP/LanguageServer+DocumentTests.swift b/CodeEditTests/Features/LSP/LanguageServer+DocumentTests.swift index 5f9aad9aaf..e4a726b57b 100644 --- a/CodeEditTests/Features/LSP/LanguageServer+DocumentTests.swift +++ b/CodeEditTests/Features/LSP/LanguageServer+DocumentTests.swift @@ -80,15 +80,48 @@ final class LanguageServerDocumentTests: XCTestCase { return (workspace, fileManager) } + func openCodeFile( + for server: LanguageServer, + connection: BufferingServerConnection, + file: CEWorkspaceFile, + syncOption: TwoTypeOption? + ) async throws -> CodeFileDocument { + let codeFile = try await CodeFileDocument( + for: file.url, + withContentsOf: file.url, + ofType: "public.swift-source" + ) + + // This is usually sent from the LSPService + try await server.openDocument(codeFile) + + await waitForClientEventCount( + 3, + connection: connection, + description: "Initialized (2) and opened (1) notification count" + ) + + // Set up full content changes + server.serverCapabilities = ServerCapabilities() + server.serverCapabilities.textDocumentSync = syncOption + + return codeFile + } + func waitForClientEventCount(_ count: Int, connection: BufferingServerConnection, description: String) async { let expectation = expectation(description: description) - Task.detached { - while connection.clientNotifications.count + connection.clientRequests.count < count { - try await Task.sleep(for: .milliseconds(10)) + + await withTaskGroup(of: Void.self) { group in + group.addTask { + await self.fulfillment(of: [expectation], timeout: 2) + } + group.addTask { + for await events in connection.clientEventSequence where events.0.count + events.1.count == count { + expectation.fulfill() + return + } } - expectation.fulfill() } - await fulfillment(of: [expectation], timeout: 2) } @MainActor @@ -96,6 +129,7 @@ final class LanguageServerDocumentTests: XCTestCase { // Set up test server let (connection, server) = try await makeTestServer() + // This service should receive the didOpen/didClose notifications let lspService = ServiceContainer.resolve(.singleton, LSPService.self) await MainActor.run { lspService?.languageClients[.init(.swift, tempTestDir.path() + "/")] = server } @@ -117,8 +151,6 @@ final class LanguageServerDocumentTests: XCTestCase { ofType: "public.swift-source" ) file.fileDocument = codeFile - - // This should trigger a documentDidOpen event CodeEditDocumentController.shared.addDocument(codeFile) await waitForClientEventCount(3, connection: connection, description: "Pre-close event count") @@ -184,36 +216,29 @@ final class LanguageServerDocumentTests: XCTestCase { for option in syncOptions { // Set up test server let (connection, server) = try await makeTestServer() - // 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" - ) - - // Set up full content changes - server.serverCapabilities = ServerCapabilities() - server.serverCapabilities.textDocumentSync = option - server.openFiles.addDocument(codeFile) - codeFile.languageServerCoordinator.languageServer = server - codeFile.languageServerCoordinator.setUpUpdatesTask() + let codeFile = try await openCodeFile(for: server, connection: connection, file: file, syncOption: option) + XCTAssertNotNil(server.openFiles.contentCoordinator(for: codeFile)) + server.openFiles.contentCoordinator(for: codeFile)?.setUpUpdatesTask() codeFile.content?.replaceString(in: .zero, with: #"func testFunction() -> String { "Hello " }"#) let textView = TextView(string: "") textView.setTextStorage(codeFile.content!) - textView.delegate = codeFile.languageServerCoordinator + textView.delegate = server.openFiles.contentCoordinator(for: codeFile) + textView.replaceCharacters(in: NSRange(location: 39, length: 0), with: "Worlld") textView.replaceCharacters(in: NSRange(location: 39, length: 6), with: "") textView.replaceCharacters(in: NSRange(location: 39, length: 0), with: "World") - await waitForClientEventCount(3, connection: connection, description: "Edited notification count") + // Added one notification + await waitForClientEventCount(4, connection: connection, description: "Edited notification count") // Make sure our text view is intact XCTAssertEqual(textView.string, #"func testFunction() -> String { "Hello World" }"#) XCTAssertEqual( [ ClientNotification.Method.initialized, + ClientNotification.Method.textDocumentDidOpen, ClientNotification.Method.textDocumentDidChange ], connection.clientNotifications.map { $0.method } @@ -230,7 +255,7 @@ final class LanguageServerDocumentTests: XCTestCase { @MainActor func testDocumentEditNotificationsIncrementalChanges() async throws { // Set up test server - let (connection, server) = try await makeTestServer() + let (_, _) = try await makeTestServer() // Set up a workspace in the temp directory let (_, fileManager) = try makeTestWorkspace() @@ -250,37 +275,28 @@ final class LanguageServerDocumentTests: XCTestCase { for option in syncOptions { // Set up test server let (connection, server) = try await makeTestServer() + let codeFile = try await openCodeFile(for: server, connection: connection, file: file, syncOption: option) - // 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" - ) - - // Set up full content changes - server.serverCapabilities = ServerCapabilities() - server.serverCapabilities.textDocumentSync = option - server.openFiles.addDocument(codeFile) - codeFile.languageServerCoordinator.languageServer = server - codeFile.languageServerCoordinator.setUpUpdatesTask() + XCTAssertNotNil(server.openFiles.contentCoordinator(for: codeFile)) + server.openFiles.contentCoordinator(for: codeFile)?.setUpUpdatesTask() codeFile.content?.replaceString(in: .zero, with: #"func testFunction() -> String { "Hello " }"#) let textView = TextView(string: "") textView.setTextStorage(codeFile.content!) - textView.delegate = codeFile.languageServerCoordinator + textView.delegate = server.openFiles.contentCoordinator(for: codeFile) textView.replaceCharacters(in: NSRange(location: 39, length: 0), with: "Worlld") textView.replaceCharacters(in: NSRange(location: 39, length: 6), with: "") textView.replaceCharacters(in: NSRange(location: 39, length: 0), with: "World") - // Throttling means we should receive one edited notification + init notification + init request - await waitForClientEventCount(3, connection: connection, description: "Edited notification count") + // Throttling means we should receive one edited notification + init notification + didOpen + init request + await waitForClientEventCount(4, connection: connection, description: "Edited notification count") // Make sure our text view is intact XCTAssertEqual(textView.string, #"func testFunction() -> String { "Hello World" }"#) XCTAssertEqual( [ ClientNotification.Method.initialized, + ClientNotification.Method.textDocumentDidOpen, ClientNotification.Method.textDocumentDidChange ], connection.clientNotifications.map { $0.method } diff --git a/CodeEditTests/Features/LSP/SemanticTokenMapTests.swift b/CodeEditTests/Features/LSP/SemanticTokenMapTests.swift new file mode 100644 index 0000000000..4c941de1a4 --- /dev/null +++ b/CodeEditTests/Features/LSP/SemanticTokenMapTests.swift @@ -0,0 +1,122 @@ +// +// SemanticTokenMapTests.swift +// CodeEditTests +// +// Created by Khan Winter on 12/14/24. +// + +import XCTest +import CodeEditSourceEditor +import LanguageServerProtocol +@testable import CodeEdit + +final class SemanticTokenMapTestsTests: XCTestCase { + // Ignores the line parameter and just returns a range from the char and length for testing + struct MockRangeProvider: SemanticTokenMapRangeProvider { + func nsRangeFrom(line: UInt32, char: UInt32, length: UInt32) -> NSRange? { + return NSRange(location: Int(char), length: Int(length)) + } + } + + let testLegend: SemanticTokensLegend = .init( + tokenTypes: [ + "include", + "constructor", + "keyword", + "boolean", + "comment", + "number" + ], + tokenModifiers: [ + "declaration", + "definition", + "readonly", + "async", + "modification", + "defaultLibrary" + ] + ) + var mockProvider: MockRangeProvider! + + override func setUp() async throws { + mockProvider = await MockRangeProvider() + } + + @MainActor + func testOptionA() { + let map = SemanticTokenMap(semanticCapability: .optionA(SemanticTokensOptions(legend: testLegend))) + + // Test decode modifiers + let modifierRaw = UInt32(0b1101) + let decodedModifiers = map.decodeModifier(modifierRaw) + XCTAssertEqual([.declaration, .readonly, .async], decodedModifiers) + + // Test decode tokens + let tokens = SemanticTokens(tokens: [ + SemanticToken(line: 0, char: 0, length: 1, type: 0, modifiers: 0b11), // First two indices set + SemanticToken(line: 0, char: 1, length: 2, type: 0, modifiers: 0b100100), // 6th and 3rd indices set + SemanticToken(line: 0, char: 4, length: 1, type: 0b1, modifiers: 0b101), + SemanticToken(line: 0, char: 5, length: 1, type: 0b100, modifiers: 0b1010), + SemanticToken(line: 0, char: 7, length: 10, type: 0, modifiers: 0) + ]) + let decoded = map.decode(tokens: tokens, using: mockProvider) + XCTAssertEqual(decoded.count, 5, "Decoded count") + + XCTAssertEqual(decoded[0].range, NSRange(location: 0, length: 1), "Decoded range") + XCTAssertEqual(decoded[1].range, NSRange(location: 1, length: 2), "Decoded range") + XCTAssertEqual(decoded[2].range, NSRange(location: 4, length: 1), "Decoded range") + XCTAssertEqual(decoded[3].range, NSRange(location: 5, length: 1), "Decoded range") + XCTAssertEqual(decoded[4].range, NSRange(location: 7, length: 10), "Decoded range") + + XCTAssertEqual(decoded[0].capture, nil, "No Decoded Capture") + XCTAssertEqual(decoded[1].capture, nil, "No Decoded Capture") + XCTAssertEqual(decoded[2].capture, .include, "Decoded Capture") + XCTAssertEqual(decoded[3].capture, .keyword, "Decoded Capture") + XCTAssertEqual(decoded[4].capture, nil, "No Decoded Capture") + + XCTAssertEqual(decoded[0].modifiers, [.declaration, .definition], "Decoded Modifiers") + XCTAssertEqual(decoded[1].modifiers, [.readonly, .defaultLibrary], "Decoded Modifiers") + XCTAssertEqual(decoded[2].modifiers, [.declaration, .readonly], "Decoded Modifiers") + XCTAssertEqual(decoded[3].modifiers, [.definition, .async], "Decoded Modifiers") + XCTAssertEqual(decoded[4].modifiers, [], "Decoded Modifiers") + } + + @MainActor + func testOptionB() { + let map = SemanticTokenMap(semanticCapability: .optionB(SemanticTokensRegistrationOptions(legend: testLegend))) + + // Test decode modifiers + let modifierRaw = UInt32(0b1101) + let decodedModifiers = map.decodeModifier(modifierRaw) + XCTAssertEqual([.declaration, .readonly, .async], decodedModifiers) + + // Test decode tokens + let tokens = SemanticTokens(tokens: [ + SemanticToken(line: 0, char: 0, length: 1, type: 0, modifiers: 0b11), // First two indices set + SemanticToken(line: 0, char: 1, length: 2, type: 0, modifiers: 0b100100), // 6th and 3rd indices set + SemanticToken(line: 0, char: 4, length: 1, type: 0b1, modifiers: 0b101), + SemanticToken(line: 0, char: 5, length: 1, type: 0b100, modifiers: 0b1010), + SemanticToken(line: 0, char: 7, length: 10, type: 0, modifiers: 0) + ]) + let decoded = map.decode(tokens: tokens, using: mockProvider) + XCTAssertEqual(decoded.count, 5, "Decoded count") + + XCTAssertEqual(decoded[0].range, NSRange(location: 0, length: 1), "Decoded range") + XCTAssertEqual(decoded[1].range, NSRange(location: 1, length: 2), "Decoded range") + XCTAssertEqual(decoded[2].range, NSRange(location: 4, length: 1), "Decoded range") + XCTAssertEqual(decoded[3].range, NSRange(location: 5, length: 1), "Decoded range") + XCTAssertEqual(decoded[4].range, NSRange(location: 7, length: 10), "Decoded range") + + XCTAssertEqual(decoded[0].capture, nil, "No Decoded Capture") + XCTAssertEqual(decoded[1].capture, nil, "No Decoded Capture") + XCTAssertEqual(decoded[2].capture, .include, "Decoded Capture") + XCTAssertEqual(decoded[3].capture, .keyword, "Decoded Capture") + XCTAssertEqual(decoded[4].capture, nil, "No Decoded Capture") + + XCTAssertEqual(decoded[0].modifiers, [.declaration, .definition], "Decoded Modifiers") + XCTAssertEqual(decoded[1].modifiers, [.readonly, .defaultLibrary], "Decoded Modifiers") + XCTAssertEqual(decoded[2].modifiers, [.declaration, .readonly], "Decoded Modifiers") + XCTAssertEqual(decoded[3].modifiers, [.definition, .async], "Decoded Modifiers") + XCTAssertEqual(decoded[4].modifiers, [], "Decoded Modifiers") + } +}