From 81745d801b3c64e9cefe1a0673b51c27ec2bcf47 Mon Sep 17 00:00:00 2001 From: Khan Winter <35942988+thecoolwinter@users.noreply.github.com> Date: Thu, 26 Dec 2024 22:09:50 -0600 Subject: [PATCH 01/18] Impl --- CodeEdit.xcodeproj/project.pbxproj | 91 ++++++++--- .../xcshareddata/swiftpm/Package.resolved | 2 +- .../CodeFileDocument/CodeFileDocument.swift | 3 + .../Features/Editor/Views/CodeFileView.swift | 4 + .../SemanticTokenHighlightProvider.swift | 116 ++++++++++++++ .../LSP/Editor/SemanticTokenMap.swift | 15 +- .../ConcreteSemanticTokenStorage.swift | 149 ++++++++++++++++++ .../SemanticTokenRange.swift | 12 ++ .../SemanticTokenStorage.swift | 24 +++ .../LanguageServer+DocumentSync.swift | 7 +- .../LanguageServer+SemanticTokens.swift | 2 + .../LSP/LanguageServer/LanguageServer.swift | 5 +- .../LanguageServerFileMap.swift | 24 ++- .../SemanticToken+Position.swift | 18 +++ .../TextView+SemanticTokenRangeProvider.swift | 5 + .../LSP/SemanticTokenStorageTests.swift | 22 +++ 16 files changed, 467 insertions(+), 32 deletions(-) create mode 100644 CodeEdit/Features/LSP/Editor/SemanticTokenHighlightProvider.swift create mode 100644 CodeEdit/Features/LSP/Editor/SemanticTokenStorage/ConcreteSemanticTokenStorage.swift create mode 100644 CodeEdit/Features/LSP/Editor/SemanticTokenStorage/SemanticTokenRange.swift create mode 100644 CodeEdit/Features/LSP/Editor/SemanticTokenStorage/SemanticTokenStorage.swift create mode 100644 CodeEdit/Utils/Extensions/SemanticToken/SemanticToken+Position.swift create mode 100644 CodeEditTests/Features/LSP/SemanticTokenStorageTests.swift diff --git a/CodeEdit.xcodeproj/project.pbxproj b/CodeEdit.xcodeproj/project.pbxproj index bf5c71f97..f68fc8c5e 100644 --- a/CodeEdit.xcodeproj/project.pbxproj +++ b/CodeEdit.xcodeproj/project.pbxproj @@ -355,7 +355,6 @@ 66F370342BEE537B00D3B823 /* NonTextFileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66F370332BEE537B00D3B823 /* NonTextFileView.swift */; }; 6C049A372A49E2DB00D42923 /* DirectoryEventStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C049A362A49E2DB00D42923 /* DirectoryEventStream.swift */; }; 6C05A8AF284D0CA3007F4EAA /* WorkspaceDocument+Listeners.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C05A8AE284D0CA3007F4EAA /* WorkspaceDocument+Listeners.swift */; }; - 6C05CF9E2CDE8699006AAECD /* CodeEditSourceEditor in Frameworks */ = {isa = PBXBuildFile; productRef = 6C05CF9D2CDE8699006AAECD /* CodeEditSourceEditor */; }; 6C0617D62BDB4432008C9C42 /* LogStream in Frameworks */ = {isa = PBXBuildFile; productRef = 6C0617D52BDB4432008C9C42 /* LogStream */; }; 6C08249C2C556F7400A0751E /* TerminalCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C08249B2C556F7400A0751E /* TerminalCache.swift */; }; 6C08249E2C55768400A0751E /* UtilityAreaTerminal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C08249D2C55768400A0751E /* UtilityAreaTerminal.swift */; }; @@ -403,6 +402,9 @@ 6C4E37F62C73DA5200AEE7B5 /* UtilityAreaTerminalSidebar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C4E37F52C73DA5200AEE7B5 /* UtilityAreaTerminalSidebar.swift */; }; 6C4E37FC2C73E00700AEE7B5 /* SwiftTerm in Frameworks */ = {isa = PBXBuildFile; productRef = 6C4E37FB2C73E00700AEE7B5 /* SwiftTerm */; }; 6C5228B529A868BD00AC48F6 /* Environment+ContentInsets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C5228B429A868BD00AC48F6 /* Environment+ContentInsets.swift */; }; + 6C52466B2D1E507500F57F11 /* SemanticTokenStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C52466A2D1E506C00F57F11 /* SemanticTokenStorageTests.swift */; }; + 6C52466D2D1E515700F57F11 /* SemanticToken+Position.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C52466C2D1E515700F57F11 /* SemanticToken+Position.swift */; }; + 6C5246702D1E5CC100F57F11 /* SemanticTokenRange.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C52466F2D1E5CC100F57F11 /* SemanticTokenRange.swift */; }; 6C53AAD829A6C4FD00EE9ED6 /* SplitView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C53AAD729A6C4FD00EE9ED6 /* SplitView.swift */; }; 6C578D8129CD294800DC73B2 /* ExtensionActivatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C578D8029CD294800DC73B2 /* ExtensionActivatorView.swift */; }; 6C578D8429CD343800DC73B2 /* ExtensionDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C578D8329CD343800DC73B2 /* ExtensionDetailView.swift */; }; @@ -444,6 +446,11 @@ 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 */; }; + 6C9AE6962D1DD71500FAE8D2 /* SemanticTokenHighlightProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C9AE6952D1DD71500FAE8D2 /* SemanticTokenHighlightProvider.swift */; }; + 6C9AE6992D1DD84600FAE8D2 /* CodeEditSourceEditor in Frameworks */ = {isa = PBXBuildFile; productRef = 6C9AE6982D1DD84600FAE8D2 /* CodeEditSourceEditor */; }; + 6C9AE69B2D1DF80300FAE8D2 /* SemanticTokenStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C9AE69A2D1DF80300FAE8D2 /* SemanticTokenStorage.swift */; }; + 6C9AE69D2D1DF84300FAE8D2 /* ConcreteSemanticTokenStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C9AE69C2D1DF84300FAE8D2 /* ConcreteSemanticTokenStorage.swift */; }; + 6C9AE6A32D1DFFDD00FAE8D2 /* CodeEditSourceEditor in Frameworks */ = {isa = PBXBuildFile; productRef = 6C9AE6A22D1DFFDD00FAE8D2 /* CodeEditSourceEditor */; }; 6CA1AE952B46950000378EAB /* EditorInstance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CA1AE942B46950000378EAB /* EditorInstance.swift */; }; 6CAAF68A29BC9C2300A1F48A /* (null) in Sources */ = {isa = PBXBuildFile; }; 6CAAF69229BCC71C00A1F48A /* (null) in Sources */ = {isa = PBXBuildFile; }; @@ -1094,6 +1101,9 @@ 6C48D8F62972E5F300D6D205 /* WindowObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowObserver.swift; sourceTree = ""; }; 6C4E37F52C73DA5200AEE7B5 /* UtilityAreaTerminalSidebar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UtilityAreaTerminalSidebar.swift; sourceTree = ""; }; 6C5228B429A868BD00AC48F6 /* Environment+ContentInsets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Environment+ContentInsets.swift"; sourceTree = ""; }; + 6C52466A2D1E506C00F57F11 /* SemanticTokenStorageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SemanticTokenStorageTests.swift; sourceTree = ""; }; + 6C52466C2D1E515700F57F11 /* SemanticToken+Position.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SemanticToken+Position.swift"; sourceTree = ""; }; + 6C52466F2D1E5CC100F57F11 /* SemanticTokenRange.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SemanticTokenRange.swift; sourceTree = ""; }; 6C53AAD729A6C4FD00EE9ED6 /* SplitView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplitView.swift; sourceTree = ""; }; 6C578D8029CD294800DC73B2 /* ExtensionActivatorView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExtensionActivatorView.swift; sourceTree = ""; }; 6C578D8329CD343800DC73B2 /* ExtensionDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtensionDetailView.swift; sourceTree = ""; }; @@ -1129,6 +1139,9 @@ 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 = ""; }; + 6C9AE6952D1DD71500FAE8D2 /* SemanticTokenHighlightProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SemanticTokenHighlightProvider.swift; sourceTree = ""; }; + 6C9AE69A2D1DF80300FAE8D2 /* SemanticTokenStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SemanticTokenStorage.swift; sourceTree = ""; }; + 6C9AE69C2D1DF84300FAE8D2 /* ConcreteSemanticTokenStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConcreteSemanticTokenStorage.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 = ""; }; @@ -1347,13 +1360,14 @@ 6C6BD6F429CD142C00235D17 /* CollectionConcurrencyKit in Frameworks */, 6C85BB442C210EFD00EB5DEF /* SwiftUIIntrospect in Frameworks */, 6CB446402B6DFF3A00539ED0 /* CodeEditSourceEditor in Frameworks */, - 6C05CF9E2CDE8699006AAECD /* CodeEditSourceEditor in Frameworks */, + 6C9AE6992D1DD84600FAE8D2 /* CodeEditSourceEditor in Frameworks */, 2816F594280CF50500DD548B /* CodeEditSymbols in Frameworks */, 30CB64942C16CA9100CC8A9E /* LanguageClient in Frameworks */, 6C6BD6F829CD14D100235D17 /* CodeEditKit in Frameworks */, 6C0824A12C5C0C9700A0751E /* SwiftTerm in Frameworks */, 6C81916B29B41DD300B75C92 /* DequeModule in Frameworks */, 6CB94D032CA1205100E8651C /* AsyncAlgorithms in Frameworks */, + 6C9AE6A32D1DFFDD00FAE8D2 /* CodeEditSourceEditor in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1614,11 +1628,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 */, + 6CD26C7C2C8EA8F400ADBA38 /* LanguageServer+DocumentSync.swift */, 30B087EB2C0D53080063A882 /* LanguageServer+FoldingRange.swift */, 30B087EC2C0D53080063A882 /* LanguageServer+Formatting.swift */, 30B087ED2C0D53080063A882 /* LanguageServer+Hover.swift */, @@ -2490,18 +2504,19 @@ children = ( 588847672992AAB800996D95 /* Array */, 5831E3C72933E7F700D5A6D2 /* Bundle */, - 5831E3C62933E7E600D5A6D2 /* Color */, 669A504F2C380BFD00304CD8 /* Collection */, + 5831E3C62933E7E600D5A6D2 /* Color */, 5831E3C82933E80500D5A6D2 /* Date */, 6CB94D002C9F1CF900E8651C /* LanguageIdentifier */, 6C82D6C429C0129E00495C54 /* NSApplication */, 5831E3D02934036D00D5A6D2 /* NSTableView */, 77A01E922BCA9C0400F0EA38 /* NSWindow */, - 6CB94CFF2C9F1CB600E8651C /* TextView */, - 77EF6C042C57DE4B00984B69 /* URL */, + 6C52466E2D1E518A00F57F11 /* SemanticToken */, 58D01C8B293167DC00C5B6B4 /* String */, 5831E3CB2933E89A00D5A6D2 /* SwiftTerm */, 6CBD1BC42978DE3E006639D5 /* Text */, + 6CB94CFF2C9F1CB600E8651C /* TextView */, + 77EF6C042C57DE4B00984B69 /* URL */, 6CD26C752C8EA80000ADBA38 /* URL */, 5831E3CA2933E86F00D5A6D2 /* View */, ); @@ -2903,7 +2918,9 @@ 6C3B4CD22D0E2C5400C6759E /* Editor */ = { isa = PBXGroup; children = ( + 6C9AE69E2D1DF8DE00FAE8D2 /* SemanticTokenStorage */, 6C278CC62C93971F0066F6D9 /* LSPContentCoordinator.swift */, + 6C9AE6952D1DD71500FAE8D2 /* SemanticTokenHighlightProvider.swift */, 6C3B4CD02D0E2C2900C6759E /* SemanticTokenMap.swift */, 6CC3D1FC2D14761A00822B65 /* SemanticTokenMapRangeProvider.swift */, ); @@ -2948,6 +2965,14 @@ path = Environment; sourceTree = ""; }; + 6C52466E2D1E518A00F57F11 /* SemanticToken */ = { + isa = PBXGroup; + children = ( + 6C52466C2D1E515700F57F11 /* SemanticToken+Position.swift */, + ); + path = SemanticToken; + sourceTree = ""; + }; 6C6BD6ED29CD123000235D17 /* Extensions */ = { isa = PBXGroup; children = ( @@ -3021,6 +3046,16 @@ path = CodeEditUITests; sourceTree = ""; }; + 6C9AE69E2D1DF8DE00FAE8D2 /* SemanticTokenStorage */ = { + isa = PBXGroup; + children = ( + 6C9AE69C2D1DF84300FAE8D2 /* ConcreteSemanticTokenStorage.swift */, + 6C52466F2D1E5CC100F57F11 /* SemanticTokenRange.swift */, + 6C9AE69A2D1DF80300FAE8D2 /* SemanticTokenStorage.swift */, + ); + path = SemanticTokenStorage; + sourceTree = ""; + }; 6CAAF68F29BCC6F900A1F48A /* WindowCommands */ = { isa = PBXGroup; children = ( @@ -3090,11 +3125,11 @@ 6CD26C732C8EA71F00ADBA38 /* LanguageServer */ = { isa = PBXGroup; children = ( + 30B0881E2C12626B0063A882 /* Capabilities */, 30B087F72C0D53080063A882 /* LanguageServer.swift */, 6CD26C6D2C8EA1E600ADBA38 /* LanguageServerFileMap.swift */, 6CD26C782C8EA8A500ADBA38 /* LSPCache.swift */, 6CD26C792C8EA8A500ADBA38 /* LSPCache+Data.swift */, - 30B0881E2C12626B0063A882 /* Capabilities */, ); path = LanguageServer; sourceTree = ""; @@ -3125,6 +3160,7 @@ 6C7D6D452C9092EC00B69EE0 /* BufferingServerConnection.swift */, 6CD26C892C8F91ED00ADBA38 /* LanguageServer+DocumentTests.swift */, 6C3B4CD32D0E2CB000C6759E /* SemanticTokenMapTests.swift */, + 6C52466A2D1E506C00F57F11 /* SemanticTokenStorageTests.swift */, ); path = LSP; sourceTree = ""; @@ -3767,7 +3803,8 @@ 6CD3CA542C8B508200D83DCD /* CodeEditSourceEditor */, 6CB94D022CA1205100E8651C /* AsyncAlgorithms */, 6CC00A8A2CBEF150004E8134 /* CodeEditSourceEditor */, - 6C05CF9D2CDE8699006AAECD /* CodeEditSourceEditor */, + 6C9AE6982D1DD84600FAE8D2 /* CodeEditSourceEditor */, + 6C9AE6A22D1DFFDD00FAE8D2 /* CodeEditSourceEditor */, ); productName = CodeEdit; productReference = B658FB2C27DA9E0F00EA4DBD /* CodeEdit.app */; @@ -3865,7 +3902,7 @@ 303E88462C276FD600EEA8D9 /* XCRemoteSwiftPackageReference "LanguageServerProtocol" */, 6C4E37FA2C73E00700AEE7B5 /* XCRemoteSwiftPackageReference "SwiftTerm" */, 6CB94D012CA1205100E8651C /* XCRemoteSwiftPackageReference "swift-async-algorithms" */, - 6C05CF9C2CDE8699006AAECD /* XCRemoteSwiftPackageReference "CodeEditSourceEditor" */, + 6C9AE6A12D1DFFDD00FAE8D2 /* XCRemoteSwiftPackageReference "CodeEditSourceEditor" */, ); productRefGroup = B658FB2D27DA9E0F00EA4DBD /* Products */; projectDirPath = ""; @@ -4019,6 +4056,7 @@ 587B9E9229301D8F00AC7927 /* BitBucketAccount.swift in Sources */, DE513F52281B672D002260B9 /* EditorTabBarAccessory.swift in Sources */, 6CD26C7A2C8EA8A500ADBA38 /* LSPCache.swift in Sources */, + 6C9AE69B2D1DF80300FAE8D2 /* SemanticTokenStorage.swift in Sources */, 618725A42C29F00400987354 /* WorkspaceMenuItemView.swift in Sources */, 2813F93927ECC4C300E305E4 /* NavigatorAreaView.swift in Sources */, B664C3B02B965F6C00816B4E /* NavigationSettings.swift in Sources */, @@ -4026,6 +4064,7 @@ 587B9E8A29301D8F00AC7927 /* GitHubIssue.swift in Sources */, EC0870F72A455F6400EB8692 /* ProjectNavigatorViewController+NSMenuDelegate.swift in Sources */, B60718202B0C6CE7009CDAB4 /* GitStashEntry.swift in Sources */, + 6C52466D2D1E515700F57F11 /* SemanticToken+Position.swift in Sources */, 6CAAF69429BCD78600A1F48A /* (null) in Sources */, 3026F50F2AC006C80061227E /* InspectorAreaViewModel.swift in Sources */, 6C82D6C629C012AD00495C54 /* NSApp+openWindow.swift in Sources */, @@ -4057,6 +4096,7 @@ 5882252E292C280D00E83CDE /* UtilityAreaMaximizeButton.swift in Sources */, 30B0880D2C0D53080063A882 /* LanguageServer+References.swift in Sources */, 77A01E2E2BB4261200F0EA38 /* CEWorkspaceSettings.swift in Sources */, + 6C9AE6962D1DD71500FAE8D2 /* SemanticTokenHighlightProvider.swift in Sources */, 6C4104E9297C970F00F472BA /* AboutDefaultView.swift in Sources */, B696A7E62CFE20C40048CFE1 /* FeatureIcon.swift in Sources */, 587B9E6F29301D8F00AC7927 /* GitLabProjectAccess.swift in Sources */, @@ -4420,6 +4460,7 @@ 6C48D8F42972DB1A00D6D205 /* Env+Window.swift in Sources */, 6C5FDF7A29E6160000BC08C0 /* AppSettings.swift in Sources */, 61A3E3DF2C3318C900076BD3 /* CEWorkspaceSettingsTaskListView.swift in Sources */, + 6C5246702D1E5CC100F57F11 /* SemanticTokenRange.swift in Sources */, 58F2EB07292FB2B0004A9BDE /* GeneralSettings.swift in Sources */, B6041F4D29D7A4E9000F3454 /* SettingsPageView.swift in Sources */, 587B9E9A29301D8F00AC7927 /* GitStatus.swift in Sources */, @@ -4434,6 +4475,7 @@ B6041F5229D7D6D6000F3454 /* SettingsWindow.swift in Sources */, 6139B9162C29B36500CA584B /* CETaskStatus.swift in Sources */, B6EA1FF829DB78DB001BF195 /* ThemeSettingThemeRow.swift in Sources */, + 6C9AE69D2D1DF84300FAE8D2 /* ConcreteSemanticTokenStorage.swift in Sources */, 587B9E7629301D8F00AC7927 /* GitTime.swift in Sources */, 587B9E5D29301D8F00AC7927 /* GitLabUserRouter.swift in Sources */, 588847692992ABCA00996D95 /* Array+SortURLs.swift in Sources */, @@ -4553,6 +4595,7 @@ 613053652B23A49300D767E3 /* TemporaryFile.swift in Sources */, 617DB3DF2C25E13800B58BFE /* TaskNotificationHandlerTests.swift in Sources */, 775566502C27FD1B001E7A4D /* CodeFileDocument+UTTypeTests.swift in Sources */, + 6C52466B2D1E507500F57F11 /* SemanticTokenStorageTests.swift in Sources */, 587B60F82934124200D5CD8F /* CEWorkspaceFileManagerTests.swift in Sources */, 6130535F2B23A31300D767E3 /* MemorySearchTests.swift in Sources */, 587B61012934170A00D5CD8F /* UnitTests_Extensions.swift in Sources */, @@ -5719,14 +5762,6 @@ version = 2.3.0; }; }; - 6C05CF9C2CDE8699006AAECD /* XCRemoteSwiftPackageReference "CodeEditSourceEditor" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/CodeEditApp/CodeEditSourceEditor"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 0.9.1; - }; - }; 6C0617D42BDB4432008C9C42 /* XCRemoteSwiftPackageReference "LogStream" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/Wouter01/LogStream"; @@ -5783,6 +5818,14 @@ minimumVersion = 1.2.0; }; }; + 6C9AE6A12D1DFFDD00FAE8D2 /* XCRemoteSwiftPackageReference "CodeEditSourceEditor" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/CodeEditApp/CodeEditSourceEditor"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 0.9.1; + }; + }; 6CB94D012CA1205100E8651C /* XCRemoteSwiftPackageReference "swift-async-algorithms" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/apple/swift-async-algorithms.git"; @@ -5819,11 +5862,6 @@ package = 58F2EB1C292FB954004A9BDE /* XCRemoteSwiftPackageReference "Sparkle" */; productName = Sparkle; }; - 6C05CF9D2CDE8699006AAECD /* CodeEditSourceEditor */ = { - isa = XCSwiftPackageProductDependency; - package = 6C05CF9C2CDE8699006AAECD /* XCRemoteSwiftPackageReference "CodeEditSourceEditor" */; - productName = CodeEditSourceEditor; - }; 6C0617D52BDB4432008C9C42 /* LogStream */ = { isa = XCSwiftPackageProductDependency; package = 6C0617D42BDB4432008C9C42 /* XCRemoteSwiftPackageReference "LogStream" */; @@ -5877,6 +5915,15 @@ package = 6C85BB422C210EFD00EB5DEF /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */; productName = SwiftUIIntrospect; }; + 6C9AE6982D1DD84600FAE8D2 /* CodeEditSourceEditor */ = { + isa = XCSwiftPackageProductDependency; + productName = CodeEditSourceEditor; + }; + 6C9AE6A22D1DFFDD00FAE8D2 /* CodeEditSourceEditor */ = { + isa = XCSwiftPackageProductDependency; + package = 6C9AE6A12D1DFFDD00FAE8D2 /* XCRemoteSwiftPackageReference "CodeEditSourceEditor" */; + productName = CodeEditSourceEditor; + }; 6CB4463F2B6DFF3A00539ED0 /* CodeEditSourceEditor */ = { isa = XCSwiftPackageProductDependency; productName = CodeEditSourceEditor; diff --git a/CodeEdit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/CodeEdit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index b65c217af..98e805383 100644 --- a/CodeEdit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/CodeEdit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "ac57a6899925c3e4ac6d43aed791c845c6fc24a4441b6a10297a207d951b7836", + "originHash" : "aef43d6aa0c467418565c574c33495a50d6e24057eb350c17704ab4ae2aead6c", "pins" : [ { "identity" : "anycodable", diff --git a/CodeEdit/Features/Documents/CodeFileDocument/CodeFileDocument.swift b/CodeEdit/Features/Documents/CodeFileDocument/CodeFileDocument.swift index 4e1638b9d..dbab39acf 100644 --- a/CodeEdit/Features/Documents/CodeFileDocument/CodeFileDocument.swift +++ b/CodeEdit/Features/Documents/CodeFileDocument/CodeFileDocument.swift @@ -53,6 +53,9 @@ final class CodeFileDocument: NSDocument, ObservableObject { /// Set by ``LanguageServer`` when initialized. @Published var lspCoordinator: LSPContentCoordinator? + /// Set by ``LanguageServer`` when initialized. + @Published var lspHighlightProvider: SemanticTokenHighlightProvider? + /// Used to override detected languages. @Published var language: CodeLanguage? diff --git a/CodeEdit/Features/Editor/Views/CodeFileView.swift b/CodeEdit/Features/Editor/Views/CodeFileView.swift index ba1fbe660..f9802f6d0 100644 --- a/CodeEdit/Features/Editor/Views/CodeFileView.swift +++ b/CodeEdit/Features/Editor/Views/CodeFileView.swift @@ -22,6 +22,8 @@ struct CodeFileView: View { /// Any coordinators passed to the view. private var textViewCoordinators: [TextViewCoordinator] + private var highlightProviders: [any HighlightProviding] + @AppSettings(\.textEditing.defaultTabWidth) var defaultTabWidth @AppSettings(\.textEditing.indentOption) @@ -59,6 +61,7 @@ struct CodeFileView: View { self.textViewCoordinators = textViewCoordinators + [codeFile.contentCoordinator] + [codeFile.lspCoordinator].compactMap({ $0 }) + self.highlightProviders = [TreeSitterClient()] + [codeFile.lspHighlightProvider].compactMap({ $0 }) self.isEditable = isEditable if let openOptions = codeFile.openOptions { @@ -129,6 +132,7 @@ struct CodeFileView: View { wrapLines: codeFile.wrapLines ?? wrapLinesToEditorWidth, cursorPositions: $cursorPositions, useThemeBackground: useThemeBackground, + highlightProviders: highlightProviders, contentInsets: edgeInsets.nsEdgeInsets, isEditable: isEditable, letterSpacing: letterSpacing, diff --git a/CodeEdit/Features/LSP/Editor/SemanticTokenHighlightProvider.swift b/CodeEdit/Features/LSP/Editor/SemanticTokenHighlightProvider.swift new file mode 100644 index 000000000..ded59bbaf --- /dev/null +++ b/CodeEdit/Features/LSP/Editor/SemanticTokenHighlightProvider.swift @@ -0,0 +1,116 @@ +// +// SemanticTokenHighlightProvider.swift +// CodeEdit +// +// Created by Khan Winter on 12/26/24. +// + +import Foundation +import LanguageServerProtocol +import CodeEditSourceEditor +import CodeEditTextView +import CodeEditLanguages + +/// Provides semantic token information from a language server for a source editor view. +/// +/// This class works in tangent with the ``LanguageServer`` class to ensure we don't unnecessarily request new tokens +/// if the document isn't updated. The ``LanguageServer`` will call the +/// ``SemanticTokenHighlightProvider/documentDidChange`` method, which in turn refreshes the semantic token storage. +/// +/// That behavior may not be intuitive due to the +/// ``SemanticTokenHighlightProvider/applyEdit(textView:range:delta:completion:)`` method. One might expect this class +/// to respond to that method immediately, but it does not. It instead stores the completion passed in that method until +/// it can respond to the edit with invalidated indices. +final class SemanticTokenHighlightProvider: HighlightProviding { + enum HighlightError: Error { + case lspRangeFailure + } + + typealias EditCallback = @MainActor (Result) -> Void + + private let tokenMap: SemanticTokenMap + private weak var languageServer: LanguageServer? + private weak var textView: TextView? + + private var lastEditCallback: EditCallback? + private var storage: Storage + + var documentRange: NSRange { + textView?.documentRange ?? .zero + } + + init(tokenMap: SemanticTokenMap, languageServer: LanguageServer) { + self.tokenMap = tokenMap + self.languageServer = languageServer + self.storage = Storage() + } + + func documentDidChange(documentURI: String) async throws { + guard let languageServer, let textView, let lastEditCallback else { return } + + // The document was updated. Update our cache and send the invalidated ranges for the editor to handle. + if let lastRequestId = storage.lastRequestId { + guard let response = try await languageServer.requestSemanticTokens( // Not sure why these are optional... + for: documentURI, + previousResultId: lastRequestId + ) else { + return + } + switch response { + case let .optionA(tokenData): + await applyEntireResponse(tokenData, callback: lastEditCallback) + case let .optionB(deltaData): + await applyDeltaResponse(deltaData, callback: lastEditCallback, textView: textView) + } + } else { + guard let response = try await languageServer.requestSemanticTokens(for: documentURI) else { + return + } + await applyEntireResponse(response, callback: lastEditCallback) + } + } + + func setUp(textView: TextView, codeLanguage: CodeLanguage) { + // Send off a request to get the initial token data + self.textView = textView + } + + func applyEdit(textView: TextView, range: NSRange, delta: Int, completion: @escaping EditCallback) { + if let lastEditCallback { + lastEditCallback(.success(IndexSet())) // Don't throw a cancellation error + } + lastEditCallback = completion + } + + func queryHighlightsFor( + textView: TextView, + range: NSRange, + completion: @escaping @MainActor (Result<[HighlightRange], any Error>) -> Void + ) { + guard let lspRange = textView.lspRangeFrom(nsRange: range) else { + completion(.failure(HighlightError.lspRangeFailure)) + return + } + let rawTokens = storage.getTokensFor(range: lspRange) + let highlights = tokenMap.decode(tokens: rawTokens, using: textView) + completion(.success(highlights)) + } + + // MARK: - Apply Response + + private func applyDeltaResponse(_ data: SemanticTokensDelta, callback: EditCallback, textView: TextView?) async { + let lspRanges = storage.applyDelta(data, requestId: data.resultId) + await MainActor.run { + let ranges = lspRanges.compactMap { textView?.nsRangeFrom($0) } + callback(.success(IndexSet(ranges: ranges))) + } + lastEditCallback = nil // Don't use this callback again. + } + + private func applyEntireResponse(_ data: SemanticTokens, callback: EditCallback) async { + storage.setData(data) + await callback(.success(IndexSet(integersIn: documentRange))) + lastEditCallback = nil // Don't use this callback again. + } + +} diff --git a/CodeEdit/Features/LSP/Editor/SemanticTokenMap.swift b/CodeEdit/Features/LSP/Editor/SemanticTokenMap.swift index 5a196cf60..ab8ceeb2d 100644 --- a/CodeEdit/Features/LSP/Editor/SemanticTokenMap.swift +++ b/CodeEdit/Features/LSP/Editor/SemanticTokenMap.swift @@ -45,12 +45,23 @@ struct SemanticTokenMap: Sendable { // swiftlint:enable line_length /// 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. + /// - tokens: Encoded semantic tokens type 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 + return decode(tokens: tokens.decode(), using: rangeProvider) + } + + /// 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: Decoded 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: [SemanticToken], using rangeProvider: SemanticTokenMapRangeProvider) -> [HighlightRange] { + tokens.compactMap { token in guard let range = rangeProvider.nsRangeFrom(line: token.line, char: token.char, length: token.length) else { return nil } diff --git a/CodeEdit/Features/LSP/Editor/SemanticTokenStorage/ConcreteSemanticTokenStorage.swift b/CodeEdit/Features/LSP/Editor/SemanticTokenStorage/ConcreteSemanticTokenStorage.swift new file mode 100644 index 000000000..46ab067b9 --- /dev/null +++ b/CodeEdit/Features/LSP/Editor/SemanticTokenStorage/ConcreteSemanticTokenStorage.swift @@ -0,0 +1,149 @@ +// +// ConcreteSemanticTokenStorage.swift +// CodeEdit +// +// Created by Khan Winter on 12/26/24. +// + +import Foundation +import LanguageServerProtocol +import CodeEditSourceEditor + +/// This class provides an efficient storage mechanism for semantic token data. +/// +/// The LSP spec requires that clients keep the original compressed data to apply delta edits to. The delta updates may +/// come as a delta to a single number in the compressed array. This class maintains a current state of compressed +/// tokens and their decoded counterparts. It supports applying delta updates from the language server. +/// +/// See ``SemanticTokenHighlightProvider`` for it's connection to the editor view. +final class ConcreteSemanticTokenStorage: SemanticTokenStorage { + struct CurrentState { + let requestId: String? + let tokenData: [UInt32] + let tokens: [SemanticToken] + } + + var lastRequestId: String? { + state?.requestId + } + + var state: CurrentState? + + init() { + state = nil + } + + // MARK: - Storage Conformance + + func getTokensFor(range: LSPRange) -> [SemanticToken] { + guard let state = state, !state.tokens.isEmpty else { + return [] + } + var tokens: [SemanticToken] = [] + + var idx = findIndex(of: range.start, data: state.tokens[...]) + while idx < state.tokens.count && state.tokens[idx].startPosition > range.end { + tokens.append(state.tokens[idx]) + idx += 1 + } + + return tokens + } + + func setData(_ data: borrowing SemanticTokens) { + state = CurrentState(requestId: nil, tokenData: data.data, tokens: data.decode()) + } + + /// Apply a delta object from a language server and returns all token ranges that may need re-drawing. + /// + /// To calculate invalidated ranges: + /// - Grabs all semantic tokens that *will* be updated and invalidates their ranges + /// - Loops over all inserted tokens and invalidates their ranges + /// This may result in duplicated ranges. It's up to the object using this method to de-duplicate if necessary. + /// + /// - Parameter deltas: The deltas to apply. + /// - Returns: All ranges invalidated by the applied deltas. + func applyDelta(_ deltas: SemanticTokensDelta, requestId: String?) -> [SemanticTokenRange] { + assert(state != nil, "State should be set before applying any deltas.") + guard var tokenData = state?.tokenData else { return [] } + var invalidatedSet: [SemanticTokenRange] = [] + + // Apply in reverse order (end to start) + for edit in deltas.edits.sorted(by: { $0.start > $1.start }) { + invalidatedSet.append( + contentsOf: invalidatedRanges(startIdx: edit.start, length: edit.deleteCount, data: tokenData[...]) + ) + + // Apply to our copy of the tokens array + if edit.deleteCount > 0 { + tokenData.replaceSubrange(Int(edit.start)..) -> [SemanticTokenRange] { + var ranges: [SemanticTokenRange] = [] + var idx = startIdx - (startIdx % 5) + while idx < startIdx + length { + ranges.append( + SemanticTokenRange( + line: data[Int(idx)], + char: data[Int(idx + 1)], + length: data[Int(idx + 2)] + ) + ) + idx += 5 + } + return ranges + } + + // MARK: - Binary Search + + /// Perform a binary search to find the given position + func findIndex(of position: Position, data: ArraySlice) -> Int { + var lower = 0 + var upper = data.count + var idx = 0 + while lower <= upper { + idx = lower + upper / 2 + if data[idx].startPosition < position { + lower = idx + 1 + } else if data[idx].startPosition > position { + upper = idx + } else { + return idx + } + } + + return idx + } +} diff --git a/CodeEdit/Features/LSP/Editor/SemanticTokenStorage/SemanticTokenRange.swift b/CodeEdit/Features/LSP/Editor/SemanticTokenStorage/SemanticTokenRange.swift new file mode 100644 index 000000000..23bc5f063 --- /dev/null +++ b/CodeEdit/Features/LSP/Editor/SemanticTokenStorage/SemanticTokenRange.swift @@ -0,0 +1,12 @@ +// +// SemanticTokenRange.swift +// CodeEdit +// +// Created by Khan Winter on 12/26/24. +// + +struct SemanticTokenRange { + let line: UInt32 + let char: UInt32 + let length: UInt32 +} diff --git a/CodeEdit/Features/LSP/Editor/SemanticTokenStorage/SemanticTokenStorage.swift b/CodeEdit/Features/LSP/Editor/SemanticTokenStorage/SemanticTokenStorage.swift new file mode 100644 index 000000000..f1d6272da --- /dev/null +++ b/CodeEdit/Features/LSP/Editor/SemanticTokenStorage/SemanticTokenStorage.swift @@ -0,0 +1,24 @@ +// +// SemanticTokenStorage.swift +// CodeEdit +// +// Created by Khan Winter on 12/26/24. +// + +import Foundation +import LanguageServerProtocol +import CodeEditSourceEditor + +/// Defines a protocol for an object to provide a storage mechanism for semantic tokens. +/// +/// There is only one concrete type that conforms to this in CE, but this protocol is used in testing. +/// See ``ConcreteSemanticTokenStorage`` for use. +protocol SemanticTokenStorage: AnyObject { + var lastRequestId: String? { get } + + init() + + func getTokensFor(range: LSPRange) -> [SemanticToken] + func setData(_ data: borrowing SemanticTokens) + func applyDelta(_ deltas: SemanticTokensDelta, requestId: String?) -> [SemanticTokenRange] +} diff --git a/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+DocumentSync.swift b/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+DocumentSync.swift index 563604aa7..ff749bace 100644 --- a/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+DocumentSync.swift +++ b/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+DocumentSync.swift @@ -29,7 +29,7 @@ extension LanguageServer { ) try await lspInstance.textDocumentDidOpen(DidOpenTextDocumentParams(textDocument: textDocument)) - await updateIsolatedDocument(document, coordinator: openFiles.contentCoordinator(for: document)) + await updateIsolatedDocument(document) } catch { logger.warning("addDocument: Error \(error)") throw error @@ -120,8 +120,9 @@ extension LanguageServer { } @MainActor - private func updateIsolatedDocument(_ document: CodeFileDocument, coordinator: LSPContentCoordinator?) { - document.lspCoordinator = coordinator + private func updateIsolatedDocument(_ document: CodeFileDocument) { + document.lspCoordinator = openFiles.contentCoordinator(for: document) + document.lspHighlightProvider = openFiles.semanticHighlighter(for: document) } // swiftlint:disable line_length diff --git a/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+SemanticTokens.swift b/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+SemanticTokens.swift index 02cb29947..e18584f9f 100644 --- a/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+SemanticTokens.swift +++ b/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+SemanticTokens.swift @@ -12,6 +12,7 @@ extension LanguageServer { /// Setup and test the validity of a rename operation at a given location func requestSemanticTokens(for documentURI: String) async throws -> SemanticTokensResponse { do { + logger.log("Requesting all tokens") let params = SemanticTokensParams( textDocument: TextDocumentIdentifier(uri: documentURI) ) @@ -40,6 +41,7 @@ extension LanguageServer { previousResultId: String ) async throws -> SemanticTokensDeltaResponse { do { + logger.log("Requesting delta tokens") let params = SemanticTokensDeltaParams( textDocument: TextDocumentIdentifier(uri: documentURI), previousResultId: previousResultId diff --git a/CodeEdit/Features/LSP/LanguageServer/LanguageServer.swift b/CodeEdit/Features/LSP/LanguageServer/LanguageServer.swift index eab8be550..0cb3d177a 100644 --- a/CodeEdit/Features/LSP/LanguageServer/LanguageServer.swift +++ b/CodeEdit/Features/LSP/LanguageServer/LanguageServer.swift @@ -56,6 +56,7 @@ class LanguageServer { category: "LanguageServer.\(languageId.rawValue)" ) if let semanticTokensProvider = serverCapabilities.semanticTokensProvider { + logger.log("Setting up semantic tokens") self.highlightMap = SemanticTokenMap(semanticCapability: semanticTokensProvider) } else { self.highlightMap = nil // Server doesn't support semantic highlights @@ -152,13 +153,13 @@ class LanguageServer { // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#semanticTokensClientCapabilities semanticTokens: SemanticTokensClientCapabilities( dynamicRegistration: false, - requests: .init(range: true, delta: true), + requests: .init(range: false, delta: true), tokenTypes: SemanticTokenTypes.allStrings, tokenModifiers: SemanticTokenModifiers.allStrings, formats: [.relative], overlappingTokenSupport: true, multilineTokenSupport: true, - serverCancelSupport: true, + serverCancelSupport: false, augmentsSyntaxTokens: true ) ) diff --git a/CodeEdit/Features/LSP/LanguageServer/LanguageServerFileMap.swift b/CodeEdit/Features/LSP/LanguageServer/LanguageServerFileMap.swift index c681e894a..271e6d0f8 100644 --- a/CodeEdit/Features/LSP/LanguageServer/LanguageServerFileMap.swift +++ b/CodeEdit/Features/LSP/LanguageServer/LanguageServerFileMap.swift @@ -15,6 +15,7 @@ class LanguageServerFileMap { let uri: String var documentVersion: Int var contentCoordinator: LSPContentCoordinator + var semanticHighlighter: SemanticTokenHighlightProvider? } private var trackedDocuments: NSMapTable @@ -29,11 +30,21 @@ class LanguageServerFileMap { func addDocument(_ document: CodeFileDocument, for server: LanguageServer) { guard let uri = document.languageServerURI else { return } trackedDocuments.setObject(document, forKey: uri as NSString) - trackedDocumentData[uri] = DocumentObject( + var docData = DocumentObject( uri: uri, documentVersion: 0, - contentCoordinator: LSPContentCoordinator(documentURI: uri, languageServer: server) + contentCoordinator: LSPContentCoordinator( + documentURI: uri, + languageServer: server + ), + semanticHighlighter: nil ) + + if let tokenMap = server.highlightMap { + docData.semanticHighlighter = .init(tokenMap: tokenMap, languageServer: server) + } + + trackedDocumentData[uri] = docData } func document(for uri: DocumentUri) -> CodeFileDocument? { @@ -82,4 +93,13 @@ class LanguageServerFileMap { func contentCoordinator(for uri: DocumentUri) -> LSPContentCoordinator? { trackedDocumentData[uri]?.contentCoordinator } + + // MARK: - Semantic Highlighter + + func semanticHighlighter( + for document: CodeFileDocument + ) -> SemanticTokenHighlightProvider? { + guard let uri = document.languageServerURI else { return nil } + return trackedDocumentData[uri]?.semanticHighlighter + } } diff --git a/CodeEdit/Utils/Extensions/SemanticToken/SemanticToken+Position.swift b/CodeEdit/Utils/Extensions/SemanticToken/SemanticToken+Position.swift new file mode 100644 index 000000000..0e700938a --- /dev/null +++ b/CodeEdit/Utils/Extensions/SemanticToken/SemanticToken+Position.swift @@ -0,0 +1,18 @@ +// +// SemanticToken+Position.swift +// CodeEdit +// +// Created by Khan Winter on 12/26/24. +// + +import LanguageServerProtocol + +extension SemanticToken { + var startPosition: Position { + Position(line: Int(line), character: Int(char)) + } + + var endPosition: Position { + Position(line: Int(line), character: Int(char + length)) + } +} diff --git a/CodeEdit/Utils/Extensions/TextView/TextView+SemanticTokenRangeProvider.swift b/CodeEdit/Utils/Extensions/TextView/TextView+SemanticTokenRangeProvider.swift index f41060423..976f9970f 100644 --- a/CodeEdit/Utils/Extensions/TextView/TextView+SemanticTokenRangeProvider.swift +++ b/CodeEdit/Utils/Extensions/TextView/TextView+SemanticTokenRangeProvider.swift @@ -7,8 +7,13 @@ import Foundation import CodeEditTextView +import LanguageServerProtocol extension TextView: SemanticTokenMapRangeProvider { + func nsRangeFrom(_ range: SemanticTokenRange) -> NSRange? { + nsRangeFrom(line: range.line, char: range.char, length: range.length) + } + func nsRangeFrom(line: UInt32, char: UInt32, length: UInt32) -> NSRange? { guard let line = layoutManager.textLineForIndex(Int(line)) else { return nil diff --git a/CodeEditTests/Features/LSP/SemanticTokenStorageTests.swift b/CodeEditTests/Features/LSP/SemanticTokenStorageTests.swift new file mode 100644 index 000000000..47c9878d9 --- /dev/null +++ b/CodeEditTests/Features/LSP/SemanticTokenStorageTests.swift @@ -0,0 +1,22 @@ +// +// SemanticTokenStorageTests.swift +// CodeEdit +// +// Created by Khan Winter on 12/26/24. +// + +import XCTest +import CodeEditSourceEditor +import LanguageServerProtocol +@testable import CodeEdit + +final class SemanticTokenStorageTests: XCTestCase { + func testInvalidation() { + let storage = ConcreteSemanticTokenStorage() + storage.state = ConcreteSemanticTokenStorage.CurrentState( + requestId: nil, + tokenData: [0, 0, 2, 0, 0], + tokens: [SemanticToken(line: 0, char: 0, length: 2, type: 0, modifiers: 0)] + ) + } +} From 359872f078981ea333c4aa822b5fceb5cab474c2 Mon Sep 17 00:00:00 2001 From: Khan Winter <35942988+thecoolwinter@users.noreply.github.com> Date: Sun, 29 Dec 2024 20:23:11 -0600 Subject: [PATCH 02/18] Why aren't events being responded to? --- CodeEdit.xcodeproj/project.pbxproj | 34 ++++++++--------- .../xcshareddata/swiftpm/Package.resolved | 37 +++---------------- .../Features/Editor/Views/CodeFileView.swift | 15 +++++++- .../LSP/Editor/LSPContentCoordinator.swift | 3 ++ .../SemanticTokenHighlightProvider.swift | 25 +++++++++---- .../LanguageServer+DocumentSync.swift | 10 ++++- .../LanguageServer+SemanticTokens.swift | 4 +- .../LSP/LanguageServer/LanguageServer.swift | 1 - .../LanguageServerFileMap.swift | 2 +- .../LSP/Service/LSPService+Events.swift | 3 ++ 10 files changed, 68 insertions(+), 66 deletions(-) diff --git a/CodeEdit.xcodeproj/project.pbxproj b/CodeEdit.xcodeproj/project.pbxproj index f68fc8c5e..44b16ed72 100644 --- a/CodeEdit.xcodeproj/project.pbxproj +++ b/CodeEdit.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 55; + objectVersion = 60; objects = { /* Begin PBXBuildFile section */ @@ -405,6 +405,7 @@ 6C52466B2D1E507500F57F11 /* SemanticTokenStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C52466A2D1E506C00F57F11 /* SemanticTokenStorageTests.swift */; }; 6C52466D2D1E515700F57F11 /* SemanticToken+Position.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C52466C2D1E515700F57F11 /* SemanticToken+Position.swift */; }; 6C5246702D1E5CC100F57F11 /* SemanticTokenRange.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C52466F2D1E5CC100F57F11 /* SemanticTokenRange.swift */; }; + 6C5246742D1E612700F57F11 /* CodeEditSourceEditor in Frameworks */ = {isa = PBXBuildFile; productRef = 6C5246732D1E612700F57F11 /* CodeEditSourceEditor */; }; 6C53AAD829A6C4FD00EE9ED6 /* SplitView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C53AAD729A6C4FD00EE9ED6 /* SplitView.swift */; }; 6C578D8129CD294800DC73B2 /* ExtensionActivatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C578D8029CD294800DC73B2 /* ExtensionActivatorView.swift */; }; 6C578D8429CD343800DC73B2 /* ExtensionDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C578D8329CD343800DC73B2 /* ExtensionDetailView.swift */; }; @@ -450,7 +451,6 @@ 6C9AE6992D1DD84600FAE8D2 /* CodeEditSourceEditor in Frameworks */ = {isa = PBXBuildFile; productRef = 6C9AE6982D1DD84600FAE8D2 /* CodeEditSourceEditor */; }; 6C9AE69B2D1DF80300FAE8D2 /* SemanticTokenStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C9AE69A2D1DF80300FAE8D2 /* SemanticTokenStorage.swift */; }; 6C9AE69D2D1DF84300FAE8D2 /* ConcreteSemanticTokenStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C9AE69C2D1DF84300FAE8D2 /* ConcreteSemanticTokenStorage.swift */; }; - 6C9AE6A32D1DFFDD00FAE8D2 /* CodeEditSourceEditor in Frameworks */ = {isa = PBXBuildFile; productRef = 6C9AE6A22D1DFFDD00FAE8D2 /* CodeEditSourceEditor */; }; 6CA1AE952B46950000378EAB /* EditorInstance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CA1AE942B46950000378EAB /* EditorInstance.swift */; }; 6CAAF68A29BC9C2300A1F48A /* (null) in Sources */ = {isa = PBXBuildFile; }; 6CAAF69229BCC71C00A1F48A /* (null) in Sources */ = {isa = PBXBuildFile; }; @@ -1367,7 +1367,7 @@ 6C0824A12C5C0C9700A0751E /* SwiftTerm in Frameworks */, 6C81916B29B41DD300B75C92 /* DequeModule in Frameworks */, 6CB94D032CA1205100E8651C /* AsyncAlgorithms in Frameworks */, - 6C9AE6A32D1DFFDD00FAE8D2 /* CodeEditSourceEditor in Frameworks */, + 6C5246742D1E612700F57F11 /* CodeEditSourceEditor in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -3804,7 +3804,7 @@ 6CB94D022CA1205100E8651C /* AsyncAlgorithms */, 6CC00A8A2CBEF150004E8134 /* CodeEditSourceEditor */, 6C9AE6982D1DD84600FAE8D2 /* CodeEditSourceEditor */, - 6C9AE6A22D1DFFDD00FAE8D2 /* CodeEditSourceEditor */, + 6C5246732D1E612700F57F11 /* CodeEditSourceEditor */, ); productName = CodeEdit; productReference = B658FB2C27DA9E0F00EA4DBD /* CodeEdit.app */; @@ -3902,7 +3902,7 @@ 303E88462C276FD600EEA8D9 /* XCRemoteSwiftPackageReference "LanguageServerProtocol" */, 6C4E37FA2C73E00700AEE7B5 /* XCRemoteSwiftPackageReference "SwiftTerm" */, 6CB94D012CA1205100E8651C /* XCRemoteSwiftPackageReference "swift-async-algorithms" */, - 6C9AE6A12D1DFFDD00FAE8D2 /* XCRemoteSwiftPackageReference "CodeEditSourceEditor" */, + 6C5246722D1E612700F57F11 /* XCLocalSwiftPackageReference "../CodeEditSourceEditor" */, ); productRefGroup = B658FB2D27DA9E0F00EA4DBD /* Products */; projectDirPath = ""; @@ -5697,6 +5697,13 @@ }; /* End XCConfigurationList section */ +/* Begin XCLocalSwiftPackageReference section */ + 6C5246722D1E612700F57F11 /* XCLocalSwiftPackageReference "../CodeEditSourceEditor" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = ../CodeEditSourceEditor; + }; +/* End XCLocalSwiftPackageReference section */ + /* Begin XCRemoteSwiftPackageReference section */ 2816F592280CF50500DD548B /* XCRemoteSwiftPackageReference "CodeEditSymbols" */ = { isa = XCRemoteSwiftPackageReference; @@ -5818,14 +5825,6 @@ minimumVersion = 1.2.0; }; }; - 6C9AE6A12D1DFFDD00FAE8D2 /* XCRemoteSwiftPackageReference "CodeEditSourceEditor" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/CodeEditApp/CodeEditSourceEditor"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 0.9.1; - }; - }; 6CB94D012CA1205100E8651C /* XCRemoteSwiftPackageReference "swift-async-algorithms" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/apple/swift-async-algorithms.git"; @@ -5881,6 +5880,10 @@ package = 6C4E37FA2C73E00700AEE7B5 /* XCRemoteSwiftPackageReference "SwiftTerm" */; productName = SwiftTerm; }; + 6C5246732D1E612700F57F11 /* CodeEditSourceEditor */ = { + isa = XCSwiftPackageProductDependency; + productName = CodeEditSourceEditor; + }; 6C66C31229D05CDC00DE9ED2 /* GRDB */ = { isa = XCSwiftPackageProductDependency; package = 6C66C31129D05CC800DE9ED2 /* XCRemoteSwiftPackageReference "GRDB.swift" */; @@ -5919,11 +5922,6 @@ isa = XCSwiftPackageProductDependency; productName = CodeEditSourceEditor; }; - 6C9AE6A22D1DFFDD00FAE8D2 /* CodeEditSourceEditor */ = { - isa = XCSwiftPackageProductDependency; - package = 6C9AE6A12D1DFFDD00FAE8D2 /* XCRemoteSwiftPackageReference "CodeEditSourceEditor" */; - productName = CodeEditSourceEditor; - }; 6CB4463F2B6DFF3A00539ED0 /* CodeEditSourceEditor */ = { isa = XCSwiftPackageProductDependency; productName = CodeEditSourceEditor; diff --git a/CodeEdit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/CodeEdit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 98e805383..db7706164 100644 --- a/CodeEdit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/CodeEdit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "aef43d6aa0c467418565c574c33495a50d6e24057eb350c17704ab4ae2aead6c", + "originHash" : "454498edc6f3f47f3616318caf54005bbbfd026d4f4355edda503b072bfe9814", "pins" : [ { "identity" : "anycodable", @@ -24,17 +24,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/CodeEditApp/CodeEditLanguages.git", "state" : { - "revision" : "331d5dbc5fc8513be5848fce8a2a312908f36a11", - "version" : "0.1.20" - } - }, - { - "identity" : "codeeditsourceeditor", - "kind" : "remoteSourceControl", - "location" : "https://github.com/CodeEditApp/CodeEditSourceEditor", - "state" : { - "revision" : "b0688fa59fb8060840fb013afb4d6e6a96000f14", - "version" : "0.9.1" + "revision" : "5b27f139269e1ea49ceae5e56dca44a3ccad50a1", + "version" : "0.1.19" } }, { @@ -46,15 +37,6 @@ "version" : "0.2.2" } }, - { - "identity" : "codeedittextview", - "kind" : "remoteSourceControl", - "location" : "https://github.com/CodeEditApp/CodeEditTextView.git", - "state" : { - "revision" : "509d7b2e86460e8ec15b0dd5410cbc8e8c05940f", - "version" : "0.7.7" - } - }, { "identity" : "collectionconcurrencykit", "kind" : "remoteSourceControl", @@ -239,8 +221,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/ChimeHQ/SwiftTreeSitter.git", "state" : { - "revision" : "36aa61d1b531f744f35229f010efba9c6d6cbbdd", - "version" : "0.9.0" + "revision" : "2599e95310b3159641469d8a21baf2d3d200e61f", + "version" : "0.8.0" } }, { @@ -269,15 +251,6 @@ "revision" : "8dc9148b46fcf93b08ea9d4ef9bdb5e4f700e008", "version" : "0.9.0" } - }, - { - "identity" : "tree-sitter", - "kind" : "remoteSourceControl", - "location" : "https://github.com/tree-sitter/tree-sitter", - "state" : { - "revision" : "d97db6d63507eb62c536bcb2c4ac7d70c8ec665e", - "version" : "0.23.2" - } } ], "version" : 3 diff --git a/CodeEdit/Features/Editor/Views/CodeFileView.swift b/CodeEdit/Features/Editor/Views/CodeFileView.swift index f9802f6d0..7b675be4d 100644 --- a/CodeEdit/Features/Editor/Views/CodeFileView.swift +++ b/CodeEdit/Features/Editor/Views/CodeFileView.swift @@ -19,10 +19,12 @@ struct CodeFileView: View { /// The current cursor positions in the view @State private var cursorPositions: [CursorPosition] = [] + @State private var treeSitterClient: TreeSitterClient = TreeSitterClient() + /// Any coordinators passed to the view. private var textViewCoordinators: [TextViewCoordinator] - private var highlightProviders: [any HighlightProviding] + @State private var highlightProviders: [any HighlightProviding] = [] @AppSettings(\.textEditing.defaultTabWidth) var defaultTabWidth @@ -58,10 +60,10 @@ struct CodeFileView: View { init(codeFile: CodeFileDocument, textViewCoordinators: [TextViewCoordinator] = [], isEditable: Bool = true) { self._codeFile = .init(wrappedValue: codeFile) + self.textViewCoordinators = textViewCoordinators + [codeFile.contentCoordinator] + [codeFile.lspCoordinator].compactMap({ $0 }) - self.highlightProviders = [TreeSitterClient()] + [codeFile.lspHighlightProvider].compactMap({ $0 }) self.isEditable = isEditable if let openOptions = codeFile.openOptions { @@ -69,6 +71,8 @@ struct CodeFileView: View { self.cursorPositions = openOptions.cursorPositions } + updateHighlightProviders() + codeFile .contentCoordinator .textUpdatePublisher @@ -158,6 +162,9 @@ struct CodeFileView: View { .onChange(of: bracketHighlight) { _ in bracketPairHighlight = getBracketPairHighlight() } + .onReceive(codeFile.$lspHighlightProvider) { provider in + updateHighlightProviders(provider) + } } private func getBracketPairHighlight() -> BracketPairHighlight? { @@ -178,6 +185,10 @@ struct CodeFileView: View { return .underline(color: color) } } + + private func updateHighlightProviders(_ lspHighlightProvider: HighlightProviding? = nil) { + highlightProviders = [lspHighlightProvider].compactMap({ $0 }) + [treeSitterClient] + } } // This extension is kept here because it should not be used elsewhere in the app and may cause confusion diff --git a/CodeEdit/Features/LSP/Editor/LSPContentCoordinator.swift b/CodeEdit/Features/LSP/Editor/LSPContentCoordinator.swift index dc17481e6..c0019e5c5 100644 --- a/CodeEdit/Features/LSP/Editor/LSPContentCoordinator.swift +++ b/CodeEdit/Features/LSP/Editor/LSPContentCoordinator.swift @@ -42,6 +42,8 @@ class LSPContentCoordinator: TextViewCoordinator, TextViewDelegate { self.stream = AsyncStream { continuation in self.sequenceContinuation = continuation } + + setUpUpdatesTask() } func setUpUpdatesTask() { @@ -50,6 +52,7 @@ class LSPContentCoordinator: TextViewCoordinator, TextViewDelegate { task = Task.detached { [weak self] in // Send edit events every 250ms for await events in stream.chunked(by: .repeating(every: .milliseconds(250), clock: .continuous)) { + guard !Task.isCancelled, self != nil else { return } guard !events.isEmpty, let uri = events.first?.uri else { continue } // Errors thrown here are already logged, not much else to do right now. diff --git a/CodeEdit/Features/LSP/Editor/SemanticTokenHighlightProvider.swift b/CodeEdit/Features/LSP/Editor/SemanticTokenHighlightProvider.swift index ded59bbaf..74e64d7b3 100644 --- a/CodeEdit/Features/LSP/Editor/SemanticTokenHighlightProvider.swift +++ b/CodeEdit/Features/LSP/Editor/SemanticTokenHighlightProvider.swift @@ -29,6 +29,7 @@ final class SemanticTokenHighlightProvider: Highl typealias EditCallback = @MainActor (Result) -> Void private let tokenMap: SemanticTokenMap + private let documentURI: String private weak var languageServer: LanguageServer? private weak var textView: TextView? @@ -39,14 +40,18 @@ final class SemanticTokenHighlightProvider: Highl textView?.documentRange ?? .zero } - init(tokenMap: SemanticTokenMap, languageServer: LanguageServer) { + init(tokenMap: SemanticTokenMap, languageServer: LanguageServer, documentURI: String) { self.tokenMap = tokenMap self.languageServer = languageServer + self.documentURI = documentURI self.storage = Storage() } - func documentDidChange(documentURI: String) async throws { - guard let languageServer, let textView, let lastEditCallback else { return } + func documentDidChange() async throws { + guard let languageServer, let textView else { + return + } + print("Doc did change") // The document was updated. Update our cache and send the invalidated ranges for the editor to handle. if let lastRequestId = storage.lastRequestId { @@ -73,6 +78,9 @@ final class SemanticTokenHighlightProvider: Highl func setUp(textView: TextView, codeLanguage: CodeLanguage) { // Send off a request to get the initial token data self.textView = textView + Task { + try await self.documentDidChange() + } } func applyEdit(textView: TextView, range: NSRange, delta: Int, completion: @escaping EditCallback) { @@ -87,6 +95,7 @@ final class SemanticTokenHighlightProvider: Highl range: NSRange, completion: @escaping @MainActor (Result<[HighlightRange], any Error>) -> Void ) { + print("Querying highlights") guard let lspRange = textView.lspRangeFrom(nsRange: range) else { completion(.failure(HighlightError.lspRangeFailure)) return @@ -98,18 +107,20 @@ final class SemanticTokenHighlightProvider: Highl // MARK: - Apply Response - private func applyDeltaResponse(_ data: SemanticTokensDelta, callback: EditCallback, textView: TextView?) async { + private func applyDeltaResponse(_ data: SemanticTokensDelta, callback: EditCallback?, textView: TextView?) async { + print("Applying delta: \(data)") let lspRanges = storage.applyDelta(data, requestId: data.resultId) await MainActor.run { let ranges = lspRanges.compactMap { textView?.nsRangeFrom($0) } - callback(.success(IndexSet(ranges: ranges))) + callback?(.success(IndexSet(ranges: ranges))) } lastEditCallback = nil // Don't use this callback again. } - private func applyEntireResponse(_ data: SemanticTokens, callback: EditCallback) async { + private func applyEntireResponse(_ data: SemanticTokens, callback: EditCallback?) async { + print("Applying entire: \(data)") storage.setData(data) - await callback(.success(IndexSet(integersIn: documentRange))) + await callback?(.success(IndexSet(integersIn: documentRange))) lastEditCallback = nil // Don't use this callback again. } diff --git a/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+DocumentSync.swift b/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+DocumentSync.swift index ff749bace..7e0599f6f 100644 --- a/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+DocumentSync.swift +++ b/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+DocumentSync.swift @@ -30,6 +30,7 @@ extension LanguageServer { try await lspInstance.textDocumentDidOpen(DidOpenTextDocumentParams(textDocument: textDocument)) await updateIsolatedDocument(document) + try await document.lspHighlightProvider?.documentDidChange() } catch { logger.warning("addDocument: Error \(error)") throw error @@ -78,10 +79,11 @@ extension LanguageServer { func documentChanged(uri: String, changes: [DocumentChange]) async throws { do { logger.debug("Document updated, \(uri, privacy: .private)") + guard let document = openFiles.document(for: uri) else { return } + switch resolveDocumentSyncKind() { case .full: - guard let document = openFiles.document(for: uri), - let content = await getIsolatedDocumentContent(document) else { + guard let content = await getIsolatedDocumentContent(document) else { return } let changeEvent = TextDocumentContentChangeEvent(range: nil, rangeLength: nil, text: content.string) @@ -100,6 +102,10 @@ extension LanguageServer { case .none: return } + + // Let the semantic token provider know about the update. + // Note for future: If a related LSP object need notifying about document changes, do it here. + try await document.lspHighlightProvider?.documentDidChange() } catch { logger.warning("closeDocument: Error \(error)") throw error diff --git a/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+SemanticTokens.swift b/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+SemanticTokens.swift index e18584f9f..570226f7e 100644 --- a/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+SemanticTokens.swift +++ b/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+SemanticTokens.swift @@ -13,9 +13,7 @@ extension LanguageServer { func requestSemanticTokens(for documentURI: String) async throws -> SemanticTokensResponse { do { logger.log("Requesting all tokens") - let params = SemanticTokensParams( - textDocument: TextDocumentIdentifier(uri: documentURI) - ) + let params = SemanticTokensParams(textDocument: TextDocumentIdentifier(uri: documentURI)) return try await lspInstance.semanticTokensFull(params) } catch { logger.warning("requestSemanticTokens full: Error \(error)") diff --git a/CodeEdit/Features/LSP/LanguageServer/LanguageServer.swift b/CodeEdit/Features/LSP/LanguageServer/LanguageServer.swift index 0cb3d177a..53fcaf2e3 100644 --- a/CodeEdit/Features/LSP/LanguageServer/LanguageServer.swift +++ b/CodeEdit/Features/LSP/LanguageServer/LanguageServer.swift @@ -56,7 +56,6 @@ class LanguageServer { category: "LanguageServer.\(languageId.rawValue)" ) if let semanticTokensProvider = serverCapabilities.semanticTokensProvider { - logger.log("Setting up semantic tokens") self.highlightMap = SemanticTokenMap(semanticCapability: semanticTokensProvider) } else { self.highlightMap = nil // Server doesn't support semantic highlights diff --git a/CodeEdit/Features/LSP/LanguageServer/LanguageServerFileMap.swift b/CodeEdit/Features/LSP/LanguageServer/LanguageServerFileMap.swift index 271e6d0f8..e476c1a01 100644 --- a/CodeEdit/Features/LSP/LanguageServer/LanguageServerFileMap.swift +++ b/CodeEdit/Features/LSP/LanguageServer/LanguageServerFileMap.swift @@ -41,7 +41,7 @@ class LanguageServerFileMap { ) if let tokenMap = server.highlightMap { - docData.semanticHighlighter = .init(tokenMap: tokenMap, languageServer: server) + docData.semanticHighlighter = .init(tokenMap: tokenMap, languageServer: server, documentURI: uri) } trackedDocumentData[uri] = docData diff --git a/CodeEdit/Features/LSP/Service/LSPService+Events.swift b/CodeEdit/Features/LSP/Service/LSPService+Events.swift index b4baa73bb..e527d6ac1 100644 --- a/CodeEdit/Features/LSP/Service/LSPService+Events.swift +++ b/CodeEdit/Features/LSP/Service/LSPService+Events.swift @@ -33,6 +33,7 @@ extension LSPService { private func handleEvent(_ event: ServerEvent, for key: ClientKey) { // TODO: Handle Events + print(event) // switch event { // case let .request(id, request): // print("Request ID: \(id) for \(key.languageId.rawValue)") @@ -46,6 +47,7 @@ extension LSPService { private func handleRequest(_ request: ServerRequest) { // TODO: Handle Requests + print(request ) // switch request { // case let .workspaceConfiguration(params, _): // print("workspaceConfiguration: \(params)") @@ -75,6 +77,7 @@ extension LSPService { private func handleNotification(_ notification: ServerNotification) { // TODO: Handle Notifications + print(notification) // switch notification { // case let .windowLogMessage(params): // print("windowLogMessage \(params.type)\n```\n\(params.message)\n```\n") From 0f429bd81a19a57ec46b80807d8eb33617564f93 Mon Sep 17 00:00:00 2001 From: Khan Winter <35942988+thecoolwinter@users.noreply.github.com> Date: Tue, 21 Jan 2025 10:05:10 -0600 Subject: [PATCH 03/18] Fix Project File --- CodeEdit.xcodeproj/project.pbxproj | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/CodeEdit.xcodeproj/project.pbxproj b/CodeEdit.xcodeproj/project.pbxproj index abb8e5701..6e96803c5 100644 --- a/CodeEdit.xcodeproj/project.pbxproj +++ b/CodeEdit.xcodeproj/project.pbxproj @@ -3002,12 +3002,6 @@ path = Environment; sourceTree = ""; }; - 6C52466E2D1E518A00F57F11 /* SemanticToken */ = { - isa = PBXGroup; - children = ( - 6C52466C2D1E515700F57F11 /* SemanticToken+Position.swift */, - ); - path = SemanticToken; 6C510CB62D2E462D006EBE85 /* Extensions */ = { isa = PBXGroup; children = ( @@ -3024,6 +3018,14 @@ path = UtilityArea; sourceTree = ""; }; + 6C52466E2D1E518A00F57F11 /* SemanticToken */ = { + isa = PBXGroup; + children = ( + 6C52466C2D1E515700F57F11 /* SemanticToken+Position.swift */, + ); + path = SemanticToken; + sourceTree = ""; + }; 6C6BD6ED29CD123000235D17 /* Extensions */ = { isa = PBXGroup; children = ( From 342548f8c9b53031a5e3be22e426969b597e8d2c Mon Sep 17 00:00:00 2001 From: Khan Winter <35942988+thecoolwinter@users.noreply.github.com> Date: Tue, 21 Jan 2025 10:14:29 -0600 Subject: [PATCH 04/18] Patch Cleaning --- CodeEdit.xcodeproj/project.pbxproj | 8 +++--- .../xcshareddata/swiftpm/Package.resolved | 28 +++++++++++++++---- .../CodeFileDocument/CodeFileDocument.swift | 2 +- ...ge.swift => LSPSemanticTokenStorage.swift} | 6 ++-- .../SemanticTokenStorage.swift | 4 +-- .../LanguageServer+DocumentSync.swift | 6 ++-- .../LanguageServerFileMap.swift | 4 +-- .../LSP/SemanticTokenStorageTests.swift | 4 +-- 8 files changed, 41 insertions(+), 21 deletions(-) rename CodeEdit/Features/LSP/Editor/SemanticTokenStorage/{ConcreteSemanticTokenStorage.swift => LSPSemanticTokenStorage.swift} (94%) diff --git a/CodeEdit.xcodeproj/project.pbxproj b/CodeEdit.xcodeproj/project.pbxproj index 6e96803c5..65b53798b 100644 --- a/CodeEdit.xcodeproj/project.pbxproj +++ b/CodeEdit.xcodeproj/project.pbxproj @@ -453,7 +453,7 @@ 6C9AE6962D1DD71500FAE8D2 /* SemanticTokenHighlightProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C9AE6952D1DD71500FAE8D2 /* SemanticTokenHighlightProvider.swift */; }; 6C9AE6992D1DD84600FAE8D2 /* CodeEditSourceEditor in Frameworks */ = {isa = PBXBuildFile; productRef = 6C9AE6982D1DD84600FAE8D2 /* CodeEditSourceEditor */; }; 6C9AE69B2D1DF80300FAE8D2 /* SemanticTokenStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C9AE69A2D1DF80300FAE8D2 /* SemanticTokenStorage.swift */; }; - 6C9AE69D2D1DF84300FAE8D2 /* ConcreteSemanticTokenStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C9AE69C2D1DF84300FAE8D2 /* ConcreteSemanticTokenStorage.swift */; }; + 6C9AE69D2D1DF84300FAE8D2 /* LSPSemanticTokenStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C9AE69C2D1DF84300FAE8D2 /* LSPSemanticTokenStorage.swift */; }; 6CA1AE952B46950000378EAB /* EditorInstance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CA1AE942B46950000378EAB /* EditorInstance.swift */; }; 6CAAF68A29BC9C2300A1F48A /* (null) in Sources */ = {isa = PBXBuildFile; }; 6CAAF69229BCC71C00A1F48A /* (null) in Sources */ = {isa = PBXBuildFile; }; @@ -1153,7 +1153,7 @@ 6C9AE6702D14A9F700FAE8D2 /* LazyServiceWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LazyServiceWrapper.swift; sourceTree = ""; }; 6C9AE6952D1DD71500FAE8D2 /* SemanticTokenHighlightProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SemanticTokenHighlightProvider.swift; sourceTree = ""; }; 6C9AE69A2D1DF80300FAE8D2 /* SemanticTokenStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SemanticTokenStorage.swift; sourceTree = ""; }; - 6C9AE69C2D1DF84300FAE8D2 /* ConcreteSemanticTokenStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConcreteSemanticTokenStorage.swift; sourceTree = ""; }; + 6C9AE69C2D1DF84300FAE8D2 /* LSPSemanticTokenStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LSPSemanticTokenStorage.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 = ""; }; @@ -3106,7 +3106,7 @@ 6C9AE69E2D1DF8DE00FAE8D2 /* SemanticTokenStorage */ = { isa = PBXGroup; children = ( - 6C9AE69C2D1DF84300FAE8D2 /* ConcreteSemanticTokenStorage.swift */, + 6C9AE69C2D1DF84300FAE8D2 /* LSPSemanticTokenStorage.swift */, 6C52466F2D1E5CC100F57F11 /* SemanticTokenRange.swift */, 6C9AE69A2D1DF80300FAE8D2 /* SemanticTokenStorage.swift */, ); @@ -4537,7 +4537,7 @@ B6041F5229D7D6D6000F3454 /* SettingsWindow.swift in Sources */, 6139B9162C29B36500CA584B /* CETaskStatus.swift in Sources */, B6EA1FF829DB78DB001BF195 /* ThemeSettingThemeRow.swift in Sources */, - 6C9AE69D2D1DF84300FAE8D2 /* ConcreteSemanticTokenStorage.swift in Sources */, + 6C9AE69D2D1DF84300FAE8D2 /* LSPSemanticTokenStorage.swift in Sources */, 587B9E7629301D8F00AC7927 /* GitTime.swift in Sources */, 587B9E5D29301D8F00AC7927 /* GitLabUserRouter.swift in Sources */, 588847692992ABCA00996D95 /* Array+SortURLs.swift in Sources */, diff --git a/CodeEdit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/CodeEdit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index db7706164..cde13bd43 100644 --- a/CodeEdit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/CodeEdit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "454498edc6f3f47f3616318caf54005bbbfd026d4f4355edda503b072bfe9814", + "originHash" : "124bcede2a81c31eed29e7b08d1f4b5324339e73b7fc32fa7eb70ef33058a6ca", "pins" : [ { "identity" : "anycodable", @@ -24,8 +24,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/CodeEditApp/CodeEditLanguages.git", "state" : { - "revision" : "5b27f139269e1ea49ceae5e56dca44a3ccad50a1", - "version" : "0.1.19" + "revision" : "331d5dbc5fc8513be5848fce8a2a312908f36a11", + "version" : "0.1.20" } }, { @@ -37,6 +37,15 @@ "version" : "0.2.2" } }, + { + "identity" : "codeedittextview", + "kind" : "remoteSourceControl", + "location" : "https://github.com/CodeEditApp/CodeEditTextView.git", + "state" : { + "revision" : "509d7b2e86460e8ec15b0dd5410cbc8e8c05940f", + "version" : "0.7.7" + } + }, { "identity" : "collectionconcurrencykit", "kind" : "remoteSourceControl", @@ -221,8 +230,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/ChimeHQ/SwiftTreeSitter.git", "state" : { - "revision" : "2599e95310b3159641469d8a21baf2d3d200e61f", - "version" : "0.8.0" + "revision" : "36aa61d1b531f744f35229f010efba9c6d6cbbdd", + "version" : "0.9.0" } }, { @@ -251,6 +260,15 @@ "revision" : "8dc9148b46fcf93b08ea9d4ef9bdb5e4f700e008", "version" : "0.9.0" } + }, + { + "identity" : "tree-sitter", + "kind" : "remoteSourceControl", + "location" : "https://github.com/tree-sitter/tree-sitter", + "state" : { + "revision" : "d97db6d63507eb62c536bcb2c4ac7d70c8ec665e", + "version" : "0.23.2" + } } ], "version" : 3 diff --git a/CodeEdit/Features/Documents/CodeFileDocument/CodeFileDocument.swift b/CodeEdit/Features/Documents/CodeFileDocument/CodeFileDocument.swift index dbab39acf..1409df2ed 100644 --- a/CodeEdit/Features/Documents/CodeFileDocument/CodeFileDocument.swift +++ b/CodeEdit/Features/Documents/CodeFileDocument/CodeFileDocument.swift @@ -54,7 +54,7 @@ final class CodeFileDocument: NSDocument, ObservableObject { @Published var lspCoordinator: LSPContentCoordinator? /// Set by ``LanguageServer`` when initialized. - @Published var lspHighlightProvider: SemanticTokenHighlightProvider? + @Published var lspHighlightProvider: SemanticTokenHighlightProvider? /// Used to override detected languages. @Published var language: CodeLanguage? diff --git a/CodeEdit/Features/LSP/Editor/SemanticTokenStorage/ConcreteSemanticTokenStorage.swift b/CodeEdit/Features/LSP/Editor/SemanticTokenStorage/LSPSemanticTokenStorage.swift similarity index 94% rename from CodeEdit/Features/LSP/Editor/SemanticTokenStorage/ConcreteSemanticTokenStorage.swift rename to CodeEdit/Features/LSP/Editor/SemanticTokenStorage/LSPSemanticTokenStorage.swift index 46ab067b9..2d167e44e 100644 --- a/CodeEdit/Features/LSP/Editor/SemanticTokenStorage/ConcreteSemanticTokenStorage.swift +++ b/CodeEdit/Features/LSP/Editor/SemanticTokenStorage/LSPSemanticTokenStorage.swift @@ -16,7 +16,8 @@ import CodeEditSourceEditor /// tokens and their decoded counterparts. It supports applying delta updates from the language server. /// /// See ``SemanticTokenHighlightProvider`` for it's connection to the editor view. -final class ConcreteSemanticTokenStorage: SemanticTokenStorage { +final class LSPSemanticTokenStorage: SemanticTokenStorage { + /// Represents compressed semantic token data received from a language server. struct CurrentState { let requestId: String? let tokenData: [UInt32] @@ -59,7 +60,7 @@ final class ConcreteSemanticTokenStorage: SemanticTokenStorage { /// To calculate invalidated ranges: /// - Grabs all semantic tokens that *will* be updated and invalidates their ranges /// - Loops over all inserted tokens and invalidates their ranges - /// This may result in duplicated ranges. It's up to the object using this method to de-duplicate if necessary. + /// This may result in duplicated ranges. It's up to the caller to de-duplicate if necessary. /// /// - Parameter deltas: The deltas to apply. /// - Returns: All ranges invalidated by the applied deltas. @@ -129,6 +130,7 @@ final class ConcreteSemanticTokenStorage: SemanticTokenStorage { // MARK: - Binary Search /// Perform a binary search to find the given position + /// - Complexity: O(log n) func findIndex(of position: Position, data: ArraySlice) -> Int { var lower = 0 var upper = data.count diff --git a/CodeEdit/Features/LSP/Editor/SemanticTokenStorage/SemanticTokenStorage.swift b/CodeEdit/Features/LSP/Editor/SemanticTokenStorage/SemanticTokenStorage.swift index f1d6272da..7b94921e2 100644 --- a/CodeEdit/Features/LSP/Editor/SemanticTokenStorage/SemanticTokenStorage.swift +++ b/CodeEdit/Features/LSP/Editor/SemanticTokenStorage/SemanticTokenStorage.swift @@ -11,8 +11,8 @@ import CodeEditSourceEditor /// Defines a protocol for an object to provide a storage mechanism for semantic tokens. /// -/// There is only one concrete type that conforms to this in CE, but this protocol is used in testing. -/// See ``ConcreteSemanticTokenStorage`` for use. +/// There is only one concrete type that conforms to this in CE, but this protocol is useful in testing. +/// See ``LSPSemanticTokenStorage`` for use. protocol SemanticTokenStorage: AnyObject { var lastRequestId: String? { get } diff --git a/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+DocumentSync.swift b/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+DocumentSync.swift index 7e0599f6f..d0779d8ad 100644 --- a/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+DocumentSync.swift +++ b/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+DocumentSync.swift @@ -29,8 +29,7 @@ extension LanguageServer { ) try await lspInstance.textDocumentDidOpen(DidOpenTextDocumentParams(textDocument: textDocument)) - await updateIsolatedDocument(document) - try await document.lspHighlightProvider?.documentDidChange() + try await updateIsolatedDocument(document) } catch { logger.warning("addDocument: Error \(error)") throw error @@ -126,9 +125,10 @@ extension LanguageServer { } @MainActor - private func updateIsolatedDocument(_ document: CodeFileDocument) { + private func updateIsolatedDocument(_ document: CodeFileDocument) async throws { document.lspCoordinator = openFiles.contentCoordinator(for: document) document.lspHighlightProvider = openFiles.semanticHighlighter(for: document) + try await document.lspHighlightProvider?.documentDidChange() } // swiftlint:disable line_length diff --git a/CodeEdit/Features/LSP/LanguageServer/LanguageServerFileMap.swift b/CodeEdit/Features/LSP/LanguageServer/LanguageServerFileMap.swift index e476c1a01..eaf66b507 100644 --- a/CodeEdit/Features/LSP/LanguageServer/LanguageServerFileMap.swift +++ b/CodeEdit/Features/LSP/LanguageServer/LanguageServerFileMap.swift @@ -15,7 +15,7 @@ class LanguageServerFileMap { let uri: String var documentVersion: Int var contentCoordinator: LSPContentCoordinator - var semanticHighlighter: SemanticTokenHighlightProvider? + var semanticHighlighter: SemanticTokenHighlightProvider? } private var trackedDocuments: NSMapTable @@ -98,7 +98,7 @@ class LanguageServerFileMap { func semanticHighlighter( for document: CodeFileDocument - ) -> SemanticTokenHighlightProvider? { + ) -> SemanticTokenHighlightProvider? { guard let uri = document.languageServerURI else { return nil } return trackedDocumentData[uri]?.semanticHighlighter } diff --git a/CodeEditTests/Features/LSP/SemanticTokenStorageTests.swift b/CodeEditTests/Features/LSP/SemanticTokenStorageTests.swift index 47c9878d9..22d9ecff3 100644 --- a/CodeEditTests/Features/LSP/SemanticTokenStorageTests.swift +++ b/CodeEditTests/Features/LSP/SemanticTokenStorageTests.swift @@ -12,8 +12,8 @@ import LanguageServerProtocol final class SemanticTokenStorageTests: XCTestCase { func testInvalidation() { - let storage = ConcreteSemanticTokenStorage() - storage.state = ConcreteSemanticTokenStorage.CurrentState( + let storage = LSPSemanticTokenStorage() + storage.state = LSPSemanticTokenStorage.CurrentState( requestId: nil, tokenData: [0, 0, 2, 0, 0], tokens: [SemanticToken(line: 0, char: 0, length: 2, type: 0, modifiers: 0)] From 9ac1b4e5dd201fba35349bb5a2b6711fbe45c7fb Mon Sep 17 00:00:00 2001 From: Khan Winter <35942988+thecoolwinter@users.noreply.github.com> Date: Wed, 29 Jan 2025 15:38:38 -0600 Subject: [PATCH 05/18] Finally Working --- .../xcshareddata/swiftpm/Package.resolved | 4 +- .../LSP/Editor/LSPContentCoordinator.swift | 1 - .../SemanticTokenHighlightProvider.swift | 132 ++++++++++++------ .../LSP/Editor/SemanticTokenMap.swift | 6 +- .../LSPSemanticTokenStorage.swift | 62 ++++---- .../SemanticTokenStorage.swift | 5 +- .../LanguageServer+DocumentSync.swift | 10 +- .../LanguageServer+SemanticTokens.swift | 1 - .../LSP/Service/LSPService+Events.swift | 3 - .../Features/LSP/Service/LSPService.swift | 2 +- 10 files changed, 135 insertions(+), 91 deletions(-) diff --git a/CodeEdit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/CodeEdit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index cde13bd43..2983746be 100644 --- a/CodeEdit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/CodeEdit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -42,8 +42,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/CodeEditApp/CodeEditTextView.git", "state" : { - "revision" : "509d7b2e86460e8ec15b0dd5410cbc8e8c05940f", - "version" : "0.7.7" + "revision" : "8ceb3fdff6c7736adcc506ce5aee4eb91973d783", + "version" : "0.7.8" } }, { diff --git a/CodeEdit/Features/LSP/Editor/LSPContentCoordinator.swift b/CodeEdit/Features/LSP/Editor/LSPContentCoordinator.swift index c0019e5c5..010e6c583 100644 --- a/CodeEdit/Features/LSP/Editor/LSPContentCoordinator.swift +++ b/CodeEdit/Features/LSP/Editor/LSPContentCoordinator.swift @@ -52,7 +52,6 @@ class LSPContentCoordinator: TextViewCoordinator, TextViewDelegate { task = Task.detached { [weak self] in // Send edit events every 250ms for await events in stream.chunked(by: .repeating(every: .milliseconds(250), clock: .continuous)) { - guard !Task.isCancelled, self != nil else { return } guard !events.isEmpty, let uri = events.first?.uri else { continue } // Errors thrown here are already logged, not much else to do right now. diff --git a/CodeEdit/Features/LSP/Editor/SemanticTokenHighlightProvider.swift b/CodeEdit/Features/LSP/Editor/SemanticTokenHighlightProvider.swift index 74e64d7b3..6a551b139 100644 --- a/CodeEdit/Features/LSP/Editor/SemanticTokenHighlightProvider.swift +++ b/CodeEdit/Features/LSP/Editor/SemanticTokenHighlightProvider.swift @@ -27,6 +27,7 @@ final class SemanticTokenHighlightProvider: Highl } typealias EditCallback = @MainActor (Result) -> Void + typealias HighlightCallback = @MainActor (Result<[HighlightRange], any Error>) -> Void private let tokenMap: SemanticTokenMap private let documentURI: String @@ -34,6 +35,7 @@ final class SemanticTokenHighlightProvider: Highl private weak var textView: TextView? private var lastEditCallback: EditCallback? + private var pendingHighlightCallbacks: [HighlightCallback] = [] private var storage: Storage var documentRange: NSRange { @@ -47,34 +49,92 @@ final class SemanticTokenHighlightProvider: Highl self.storage = Storage() } + // MARK: - Language Server Content Lifecycle + + /// Called when the language server finishes sending a document update. + /// + /// This method first checks if this object has any semantic tokens. If not, requests new tokens and responds to the + /// `pendingHighlightCallbacks` queue with cancellation errors, causing the highlighter to re-query those indices. + /// + /// If this object already has some tokens, it determines whether or not we can request a token delta and + /// performs the request. func documentDidChange() async throws { guard let languageServer, let textView else { return } - print("Doc did change") - - // The document was updated. Update our cache and send the invalidated ranges for the editor to handle. - if let lastRequestId = storage.lastRequestId { - guard let response = try await languageServer.requestSemanticTokens( // Not sure why these are optional... - for: documentURI, - previousResultId: lastRequestId - ) else { - return - } - switch response { - case let .optionA(tokenData): - await applyEntireResponse(tokenData, callback: lastEditCallback) - case let .optionB(deltaData): - await applyDeltaResponse(deltaData, callback: lastEditCallback, textView: textView) - } - } else { - guard let response = try await languageServer.requestSemanticTokens(for: documentURI) else { - return + + guard storage.hasTokens else { + // We have no semantic token info, request it! + try await requestTokens(languageServer: languageServer, textView: textView) + await MainActor.run { + for callback in pendingHighlightCallbacks { + callback(.failure(HighlightProvidingError.operationCancelled)) + } + pendingHighlightCallbacks.removeAll() } - await applyEntireResponse(response, callback: lastEditCallback) + return + } + + // The document was updated. Update our token cache and send the invalidated ranges for the editor to handle. + if let lastResultId = storage.lastResultId { + try await requestDeltaTokens(languageServer: languageServer, textView: textView, lastResultId: lastResultId) + return + } + + try await requestTokens(languageServer: languageServer, textView: textView) + } + + // MARK: - LSP Token Requests + + /// Requests and applies a token delta. Requires a previous response identifier. + private func requestDeltaTokens( + languageServer: LanguageServer, + textView: TextView, + lastResultId: String + ) async throws { + guard let response = try await languageServer.requestSemanticTokens( + for: documentURI, + previousResultId: lastResultId + ) else { + return + } + switch response { + case let .optionA(tokenData): + await applyEntireResponse(tokenData, callback: lastEditCallback) + case let .optionB(deltaData): + await applyDeltaResponse(deltaData, callback: lastEditCallback, textView: textView) + } + } + + /// Requests and applies tokens for an entire document. This does not require a previous response id, and should be + /// used in place of `requestDeltaTokens` when that's the case. + private func requestTokens(languageServer: LanguageServer, textView: TextView) async throws { + guard let response = try await languageServer.requestSemanticTokens(for: documentURI) else { + return + } + await applyEntireResponse(response, callback: lastEditCallback) + } + + // MARK: - Apply LSP Response + + /// Applies a delta response from the LSP to our storage. + private func applyDeltaResponse(_ data: SemanticTokensDelta, callback: EditCallback?, textView: TextView?) async { + let lspRanges = storage.applyDelta(data) + lastEditCallback = nil // Don't use this callback again. + await MainActor.run { + let ranges = lspRanges.compactMap { textView?.nsRangeFrom($0) } + callback?(.success(IndexSet(ranges: ranges))) } } + private func applyEntireResponse(_ data: SemanticTokens, callback: EditCallback?) async { + storage.setData(data) + lastEditCallback = nil // Don't use this callback again. + await callback?(.success(IndexSet(integersIn: documentRange))) + } + + // MARK: - Highlight Provider Conformance + func setUp(textView: TextView, codeLanguage: CodeLanguage) { // Send off a request to get the initial token data self.textView = textView @@ -90,12 +150,12 @@ final class SemanticTokenHighlightProvider: Highl lastEditCallback = completion } - func queryHighlightsFor( - textView: TextView, - range: NSRange, - completion: @escaping @MainActor (Result<[HighlightRange], any Error>) -> Void - ) { - print("Querying highlights") + func queryHighlightsFor(textView: TextView, range: NSRange, completion: @escaping HighlightCallback) { + guard storage.hasTokens else { + pendingHighlightCallbacks.append(completion) + return + } + guard let lspRange = textView.lspRangeFrom(nsRange: range) else { completion(.failure(HighlightError.lspRangeFailure)) return @@ -104,24 +164,4 @@ final class SemanticTokenHighlightProvider: Highl let highlights = tokenMap.decode(tokens: rawTokens, using: textView) completion(.success(highlights)) } - - // MARK: - Apply Response - - private func applyDeltaResponse(_ data: SemanticTokensDelta, callback: EditCallback?, textView: TextView?) async { - print("Applying delta: \(data)") - let lspRanges = storage.applyDelta(data, requestId: data.resultId) - await MainActor.run { - let ranges = lspRanges.compactMap { textView?.nsRangeFrom($0) } - callback?(.success(IndexSet(ranges: ranges))) - } - lastEditCallback = nil // Don't use this callback again. - } - - private func applyEntireResponse(_ data: SemanticTokens, callback: EditCallback?) async { - print("Applying entire: \(data)") - storage.setData(data) - await callback?(.success(IndexSet(integersIn: documentRange))) - lastEditCallback = nil // Don't use this callback again. - } - } diff --git a/CodeEdit/Features/LSP/Editor/SemanticTokenMap.swift b/CodeEdit/Features/LSP/Editor/SemanticTokenMap.swift index ab8ceeb2d..0e580d22d 100644 --- a/CodeEdit/Features/LSP/Editor/SemanticTokenMap.swift +++ b/CodeEdit/Features/LSP/Editor/SemanticTokenMap.swift @@ -66,12 +66,14 @@ struct SemanticTokenMap: Sendable { // swiftlint:enable line_length return nil } + // Only modifiers are bit packed, capture types are given as a simple index into the ``tokenTypeMap`` 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 type = Int(token.type) let capture = tokenTypeMap.indices.contains(type) ? tokenTypeMap[type] : nil +// print(token.line, token.char, token.length, range, capture, modifiers.values) + return HighlightRange( range: range, capture: capture, diff --git a/CodeEdit/Features/LSP/Editor/SemanticTokenStorage/LSPSemanticTokenStorage.swift b/CodeEdit/Features/LSP/Editor/SemanticTokenStorage/LSPSemanticTokenStorage.swift index 2d167e44e..94d102018 100644 --- a/CodeEdit/Features/LSP/Editor/SemanticTokenStorage/LSPSemanticTokenStorage.swift +++ b/CodeEdit/Features/LSP/Editor/SemanticTokenStorage/LSPSemanticTokenStorage.swift @@ -19,13 +19,17 @@ import CodeEditSourceEditor final class LSPSemanticTokenStorage: SemanticTokenStorage { /// Represents compressed semantic token data received from a language server. struct CurrentState { - let requestId: String? + let resultId: String? let tokenData: [UInt32] let tokens: [SemanticToken] } - var lastRequestId: String? { - state?.requestId + var lastResultId: String? { + state?.resultId + } + + var hasTokens: Bool { + state != nil } var state: CurrentState? @@ -42,17 +46,18 @@ final class LSPSemanticTokenStorage: SemanticTokenStorage { } var tokens: [SemanticToken] = [] - var idx = findIndex(of: range.start, data: state.tokens[...]) - while idx < state.tokens.count && state.tokens[idx].startPosition > range.end { - tokens.append(state.tokens[idx]) - idx += 1 - } +// var idx = findLowerBound(of: range.start, data: state.tokens[...]) +// while idx < state.tokens.count && state.tokens[idx].startPosition < range.end { +// tokens.append(state.tokens[idx]) +// idx += 1 +// } return tokens } func setData(_ data: borrowing SemanticTokens) { - state = CurrentState(requestId: nil, tokenData: data.data, tokens: data.decode()) + print(data.decode()) + state = CurrentState(resultId: data.resultId, tokenData: data.data, tokens: data.decode()) } /// Apply a delta object from a language server and returns all token ranges that may need re-drawing. @@ -64,7 +69,7 @@ final class LSPSemanticTokenStorage: SemanticTokenStorage { /// /// - Parameter deltas: The deltas to apply. /// - Returns: All ranges invalidated by the applied deltas. - func applyDelta(_ deltas: SemanticTokensDelta, requestId: String?) -> [SemanticTokenRange] { + func applyDelta(_ deltas: SemanticTokensDelta) -> [SemanticTokenRange] { assert(state != nil, "State should be set before applying any deltas.") guard var tokenData = state?.tokenData else { return [] } var invalidatedSet: [SemanticTokenRange] = [] @@ -104,7 +109,7 @@ final class LSPSemanticTokenStorage: SemanticTokenStorage { modifiers: tokenData[idx + 4] )) } - state = CurrentState(requestId: requestId, tokenData: tokenData, tokens: decodedTokens) + state = CurrentState(resultId: deltas.resultId, tokenData: tokenData, tokens: decodedTokens) return invalidatedSet } @@ -131,21 +136,24 @@ final class LSPSemanticTokenStorage: SemanticTokenStorage { /// Perform a binary search to find the given position /// - Complexity: O(log n) - func findIndex(of position: Position, data: ArraySlice) -> Int { - var lower = 0 - var upper = data.count - var idx = 0 - while lower <= upper { - idx = lower + upper / 2 - if data[idx].startPosition < position { - lower = idx + 1 - } else if data[idx].startPosition > position { - upper = idx - } else { - return idx - } - } - - return idx + func findLowerBound(in range: LSPRange, data: ArraySlice) -> Int? { + // TODO: This needs to find the closest value in a range, there's a good chance there's no result for a + // specific indice +// var lower = 0 +// var upper = data.count +// var idx = 0 +// while lower < upper { +// idx = lower + upper / 2 +// if data[idx].startPosition < position { +// lower = idx + 1 +// } else if data[idx].startPosition > position { +// upper = idx +// } else { +// return idx +// } +// } +// +// return (data[idx].startPosition.. [SemanticToken] func setData(_ data: borrowing SemanticTokens) - func applyDelta(_ deltas: SemanticTokensDelta, requestId: String?) -> [SemanticTokenRange] + func applyDelta(_ deltas: SemanticTokensDelta) -> [SemanticTokenRange] } diff --git a/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+DocumentSync.swift b/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+DocumentSync.swift index d0779d8ad..5beb130f1 100644 --- a/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+DocumentSync.swift +++ b/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+DocumentSync.swift @@ -29,7 +29,7 @@ extension LanguageServer { ) try await lspInstance.textDocumentDidOpen(DidOpenTextDocumentParams(textDocument: textDocument)) - try await updateIsolatedDocument(document) + await updateIsolatedDocument(document) } catch { logger.warning("addDocument: Error \(error)") throw error @@ -117,18 +117,16 @@ extension LanguageServer { @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, string: content) + return DocumentContent(uri: uri, language: document.getLanguage().id.rawValue, string: content) } @MainActor - private func updateIsolatedDocument(_ document: CodeFileDocument) async throws { + private func updateIsolatedDocument(_ document: CodeFileDocument) { document.lspCoordinator = openFiles.contentCoordinator(for: document) document.lspHighlightProvider = openFiles.semanticHighlighter(for: document) - try await document.lspHighlightProvider?.documentDidChange() } // swiftlint:disable line_length @@ -163,7 +161,7 @@ extension LanguageServer { // Used to avoid a lint error (`large_tuple`) for the return type of `getIsolatedDocumentContent` fileprivate struct DocumentContent { let uri: String - let language: LanguageIdentifier + let language: String let string: String } } diff --git a/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+SemanticTokens.swift b/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+SemanticTokens.swift index 570226f7e..e6242a0b8 100644 --- a/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+SemanticTokens.swift +++ b/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+SemanticTokens.swift @@ -9,7 +9,6 @@ import Foundation import LanguageServerProtocol extension LanguageServer { - /// Setup and test the validity of a rename operation at a given location func requestSemanticTokens(for documentURI: String) async throws -> SemanticTokensResponse { do { logger.log("Requesting all tokens") diff --git a/CodeEdit/Features/LSP/Service/LSPService+Events.swift b/CodeEdit/Features/LSP/Service/LSPService+Events.swift index e527d6ac1..b4baa73bb 100644 --- a/CodeEdit/Features/LSP/Service/LSPService+Events.swift +++ b/CodeEdit/Features/LSP/Service/LSPService+Events.swift @@ -33,7 +33,6 @@ extension LSPService { private func handleEvent(_ event: ServerEvent, for key: ClientKey) { // TODO: Handle Events - print(event) // switch event { // case let .request(id, request): // print("Request ID: \(id) for \(key.languageId.rawValue)") @@ -47,7 +46,6 @@ extension LSPService { private func handleRequest(_ request: ServerRequest) { // TODO: Handle Requests - print(request ) // switch request { // case let .workspaceConfiguration(params, _): // print("workspaceConfiguration: \(params)") @@ -77,7 +75,6 @@ extension LSPService { private func handleNotification(_ notification: ServerNotification) { // TODO: Handle Notifications - print(notification) // switch notification { // case let .windowLogMessage(params): // print("windowLogMessage \(params.type)\n```\n\(params.message)\n```\n") diff --git a/CodeEdit/Features/LSP/Service/LSPService.swift b/CodeEdit/Features/LSP/Service/LSPService.swift index 2eaab98d6..5110c6c3e 100644 --- a/CodeEdit/Features/LSP/Service/LSPService.swift +++ b/CodeEdit/Features/LSP/Service/LSPService.swift @@ -218,7 +218,7 @@ final class LSPService: ObservableObject { do { try await languageServer.openDocument(document) } catch { - let uri = await document.languageServerURI + let uri = document.languageServerURI // swiftlint:disable:next line_length self.logger.error("Failed to close document: \(uri ?? "", privacy: .private), language: \(lspLanguage.rawValue). Error \(error)") } From 61a52cfc9bdaededca28a8ee32414e336656cfd2 Mon Sep 17 00:00:00 2001 From: Khan Winter <35942988+thecoolwinter@users.noreply.github.com> Date: Wed, 12 Feb 2025 16:11:44 -0600 Subject: [PATCH 06/18] Organization, Implement Correct Binary Search --- CodeEdit.xcodeproj/project.pbxproj | 54 ++++++++------- .../xcshareddata/swiftpm/Package.resolved | 65 +------------------ .../CodeFileDocument/CodeFileDocument.swift | 2 +- .../DocumentSync}/LSPContentCoordinator.swift | 0 .../SemanticTokenHighlightProvider.swift | 2 +- .../SemanticTokens}/SemanticTokenMap.swift | 0 .../SemanticTokenMapRangeProvider.swift | 0 .../GenericSemanticTokenStorage.swift} | 6 +- .../SemanticTokenRange.swift | 0 .../SemanticTokenStorage.swift} | 62 ++++++++++-------- .../LanguageServerFileMap.swift | 4 +- 11 files changed, 75 insertions(+), 120 deletions(-) rename CodeEdit/Features/LSP/{Editor => Features/DocumentSync}/LSPContentCoordinator.swift (100%) rename CodeEdit/Features/LSP/{Editor => Features/SemanticTokens}/SemanticTokenHighlightProvider.swift (98%) rename CodeEdit/Features/LSP/{Editor => Features/SemanticTokens}/SemanticTokenMap.swift (100%) rename CodeEdit/Features/LSP/{Editor => Features/SemanticTokens}/SemanticTokenMapRangeProvider.swift (100%) rename CodeEdit/Features/LSP/{Editor/SemanticTokenStorage/SemanticTokenStorage.swift => Features/SemanticTokens/SemanticTokenStorage/GenericSemanticTokenStorage.swift} (82%) rename CodeEdit/Features/LSP/{Editor => Features/SemanticTokens}/SemanticTokenStorage/SemanticTokenRange.swift (100%) rename CodeEdit/Features/LSP/{Editor/SemanticTokenStorage/LSPSemanticTokenStorage.swift => Features/SemanticTokens/SemanticTokenStorage/SemanticTokenStorage.swift} (75%) diff --git a/CodeEdit.xcodeproj/project.pbxproj b/CodeEdit.xcodeproj/project.pbxproj index 65b53798b..278ee83c0 100644 --- a/CodeEdit.xcodeproj/project.pbxproj +++ b/CodeEdit.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 60; + objectVersion = 55; objects = { /* Begin PBXBuildFile section */ @@ -452,8 +452,8 @@ 6C9AE6712D14A9F700FAE8D2 /* LazyServiceWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C9AE6702D14A9F700FAE8D2 /* LazyServiceWrapper.swift */; }; 6C9AE6962D1DD71500FAE8D2 /* SemanticTokenHighlightProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C9AE6952D1DD71500FAE8D2 /* SemanticTokenHighlightProvider.swift */; }; 6C9AE6992D1DD84600FAE8D2 /* CodeEditSourceEditor in Frameworks */ = {isa = PBXBuildFile; productRef = 6C9AE6982D1DD84600FAE8D2 /* CodeEditSourceEditor */; }; - 6C9AE69B2D1DF80300FAE8D2 /* SemanticTokenStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C9AE69A2D1DF80300FAE8D2 /* SemanticTokenStorage.swift */; }; - 6C9AE69D2D1DF84300FAE8D2 /* LSPSemanticTokenStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C9AE69C2D1DF84300FAE8D2 /* LSPSemanticTokenStorage.swift */; }; + 6C9AE69B2D1DF80300FAE8D2 /* GenericSemanticTokenStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C9AE69A2D1DF80300FAE8D2 /* GenericSemanticTokenStorage.swift */; }; + 6C9AE69D2D1DF84300FAE8D2 /* SemanticTokenStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C9AE69C2D1DF84300FAE8D2 /* SemanticTokenStorage.swift */; }; 6CA1AE952B46950000378EAB /* EditorInstance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CA1AE942B46950000378EAB /* EditorInstance.swift */; }; 6CAAF68A29BC9C2300A1F48A /* (null) in Sources */ = {isa = PBXBuildFile; }; 6CAAF69229BCC71C00A1F48A /* (null) in Sources */ = {isa = PBXBuildFile; }; @@ -1152,8 +1152,8 @@ 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 = ""; }; 6C9AE6952D1DD71500FAE8D2 /* SemanticTokenHighlightProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SemanticTokenHighlightProvider.swift; sourceTree = ""; }; - 6C9AE69A2D1DF80300FAE8D2 /* SemanticTokenStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SemanticTokenStorage.swift; sourceTree = ""; }; - 6C9AE69C2D1DF84300FAE8D2 /* LSPSemanticTokenStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LSPSemanticTokenStorage.swift; sourceTree = ""; }; + 6C9AE69A2D1DF80300FAE8D2 /* GenericSemanticTokenStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenericSemanticTokenStorage.swift; sourceTree = ""; }; + 6C9AE69C2D1DF84300FAE8D2 /* SemanticTokenStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SemanticTokenStorage.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 = ""; }; @@ -1629,10 +1629,10 @@ 30B087FB2C0D53080063A882 /* LSP */ = { isa = PBXGroup; children = ( - 6C3B4CD22D0E2C5400C6759E /* Editor */, + 30B087FA2C0D53080063A882 /* LSPUtil.swift */, + 6C1A7E932D5D508A001B951C /* Features */, 6CD26C732C8EA71F00ADBA38 /* LanguageServer */, 6CD26C742C8EA79100ADBA38 /* Service */, - 30B087FA2C0D53080063A882 /* LSPUtil.swift */, ); path = LSP; sourceTree = ""; @@ -2943,6 +2943,23 @@ path = FindNavigatorResultList; sourceTree = ""; }; + 6C1A7E922D5D5083001B951C /* DocumentSync */ = { + isa = PBXGroup; + children = ( + 6C278CC62C93971F0066F6D9 /* LSPContentCoordinator.swift */, + ); + path = DocumentSync; + sourceTree = ""; + }; + 6C1A7E932D5D508A001B951C /* Features */ = { + isa = PBXGroup; + children = ( + 6C1A7E922D5D5083001B951C /* DocumentSync */, + 6C3B4CD22D0E2C5400C6759E /* SemanticTokens */, + ); + path = Features; + sourceTree = ""; + }; 6C2384302C796EBD003FBDD4 /* ChangedFile */ = { isa = PBXGroup; children = ( @@ -2952,16 +2969,15 @@ path = ChangedFile; sourceTree = ""; }; - 6C3B4CD22D0E2C5400C6759E /* Editor */ = { + 6C3B4CD22D0E2C5400C6759E /* SemanticTokens */ = { isa = PBXGroup; children = ( - 6C9AE69E2D1DF8DE00FAE8D2 /* SemanticTokenStorage */, - 6C278CC62C93971F0066F6D9 /* LSPContentCoordinator.swift */, 6C9AE6952D1DD71500FAE8D2 /* SemanticTokenHighlightProvider.swift */, 6C3B4CD02D0E2C2900C6759E /* SemanticTokenMap.swift */, 6CC3D1FC2D14761A00822B65 /* SemanticTokenMapRangeProvider.swift */, + 6C9AE69E2D1DF8DE00FAE8D2 /* SemanticTokenStorage */, ); - path = Editor; + path = SemanticTokens; sourceTree = ""; }; 6C3E12D42CC830DE00DD12F1 /* Model */ = { @@ -3106,9 +3122,9 @@ 6C9AE69E2D1DF8DE00FAE8D2 /* SemanticTokenStorage */ = { isa = PBXGroup; children = ( - 6C9AE69C2D1DF84300FAE8D2 /* LSPSemanticTokenStorage.swift */, + 6C9AE69A2D1DF80300FAE8D2 /* GenericSemanticTokenStorage.swift */, 6C52466F2D1E5CC100F57F11 /* SemanticTokenRange.swift */, - 6C9AE69A2D1DF80300FAE8D2 /* SemanticTokenStorage.swift */, + 6C9AE69C2D1DF84300FAE8D2 /* SemanticTokenStorage.swift */, ); path = SemanticTokenStorage; sourceTree = ""; @@ -3960,7 +3976,6 @@ 303E88462C276FD600EEA8D9 /* XCRemoteSwiftPackageReference "LanguageServerProtocol" */, 6C4E37FA2C73E00700AEE7B5 /* XCRemoteSwiftPackageReference "SwiftTerm" */, 6CB94D012CA1205100E8651C /* XCRemoteSwiftPackageReference "swift-async-algorithms" */, - 6C5246722D1E612700F57F11 /* XCLocalSwiftPackageReference "../CodeEditSourceEditor" */, ); productRefGroup = B658FB2D27DA9E0F00EA4DBD /* Products */; projectDirPath = ""; @@ -4115,7 +4130,7 @@ 587B9E9229301D8F00AC7927 /* BitBucketAccount.swift in Sources */, DE513F52281B672D002260B9 /* EditorTabBarAccessory.swift in Sources */, 6CD26C7A2C8EA8A500ADBA38 /* LSPCache.swift in Sources */, - 6C9AE69B2D1DF80300FAE8D2 /* SemanticTokenStorage.swift in Sources */, + 6C9AE69B2D1DF80300FAE8D2 /* GenericSemanticTokenStorage.swift in Sources */, 618725A42C29F00400987354 /* WorkspaceMenuItemView.swift in Sources */, 2813F93927ECC4C300E305E4 /* NavigatorAreaView.swift in Sources */, B664C3B02B965F6C00816B4E /* NavigationSettings.swift in Sources */, @@ -4537,7 +4552,7 @@ B6041F5229D7D6D6000F3454 /* SettingsWindow.swift in Sources */, 6139B9162C29B36500CA584B /* CETaskStatus.swift in Sources */, B6EA1FF829DB78DB001BF195 /* ThemeSettingThemeRow.swift in Sources */, - 6C9AE69D2D1DF84300FAE8D2 /* LSPSemanticTokenStorage.swift in Sources */, + 6C9AE69D2D1DF84300FAE8D2 /* SemanticTokenStorage.swift in Sources */, 587B9E7629301D8F00AC7927 /* GitTime.swift in Sources */, 587B9E5D29301D8F00AC7927 /* GitLabUserRouter.swift in Sources */, 588847692992ABCA00996D95 /* Array+SortURLs.swift in Sources */, @@ -5764,13 +5779,6 @@ }; /* End XCConfigurationList section */ -/* Begin XCLocalSwiftPackageReference section */ - 6C5246722D1E612700F57F11 /* XCLocalSwiftPackageReference "../CodeEditSourceEditor" */ = { - isa = XCLocalSwiftPackageReference; - relativePath = ../CodeEditSourceEditor; - }; -/* End XCLocalSwiftPackageReference section */ - /* Begin XCRemoteSwiftPackageReference section */ 2816F592280CF50500DD548B /* XCRemoteSwiftPackageReference "CodeEditSymbols" */ = { isa = XCRemoteSwiftPackageReference; diff --git a/CodeEdit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/CodeEdit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 2983746be..35ce38368 100644 --- a/CodeEdit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/CodeEdit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "124bcede2a81c31eed29e7b08d1f4b5324339e73b7fc32fa7eb70ef33058a6ca", + "originHash" : "6d67b1d6a6b95c05b6bdfe0b5815724ee9febd65e03425065104a94686d775f5", "pins" : [ { "identity" : "anycodable", @@ -19,15 +19,6 @@ "version" : "0.1.2" } }, - { - "identity" : "codeeditlanguages", - "kind" : "remoteSourceControl", - "location" : "https://github.com/CodeEditApp/CodeEditLanguages.git", - "state" : { - "revision" : "331d5dbc5fc8513be5848fce8a2a312908f36a11", - "version" : "0.1.20" - } - }, { "identity" : "codeeditsymbols", "kind" : "remoteSourceControl", @@ -37,15 +28,6 @@ "version" : "0.2.2" } }, - { - "identity" : "codeedittextview", - "kind" : "remoteSourceControl", - "location" : "https://github.com/CodeEditApp/CodeEditTextView.git", - "state" : { - "revision" : "8ceb3fdff6c7736adcc506ce5aee4eb91973d783", - "version" : "0.7.8" - } - }, { "identity" : "collectionconcurrencykit", "kind" : "remoteSourceControl", @@ -145,15 +127,6 @@ "version" : "0.1.4" } }, - { - "identity" : "rearrange", - "kind" : "remoteSourceControl", - "location" : "https://github.com/ChimeHQ/Rearrange", - "state" : { - "revision" : "5ff7f3363f7a08f77e0d761e38e6add31c2136e1", - "version" : "1.8.1" - } - }, { "identity" : "semaphore", "kind" : "remoteSourceControl", @@ -225,15 +198,6 @@ "revision" : "384776a4e24d08833ac7c6b8c6f6c7490323c845" } }, - { - "identity" : "swifttreesitter", - "kind" : "remoteSourceControl", - "location" : "https://github.com/ChimeHQ/SwiftTreeSitter.git", - "state" : { - "revision" : "36aa61d1b531f744f35229f010efba9c6d6cbbdd", - "version" : "0.9.0" - } - }, { "identity" : "swiftui-introspect", "kind" : "remoteSourceControl", @@ -242,33 +206,6 @@ "revision" : "807f73ce09a9b9723f12385e592b4e0aaebd3336", "version" : "1.3.0" } - }, - { - "identity" : "textformation", - "kind" : "remoteSourceControl", - "location" : "https://github.com/ChimeHQ/TextFormation", - "state" : { - "revision" : "b1ce9a14bd86042bba4de62236028dc4ce9db6a1", - "version" : "0.9.0" - } - }, - { - "identity" : "textstory", - "kind" : "remoteSourceControl", - "location" : "https://github.com/ChimeHQ/TextStory", - "state" : { - "revision" : "8dc9148b46fcf93b08ea9d4ef9bdb5e4f700e008", - "version" : "0.9.0" - } - }, - { - "identity" : "tree-sitter", - "kind" : "remoteSourceControl", - "location" : "https://github.com/tree-sitter/tree-sitter", - "state" : { - "revision" : "d97db6d63507eb62c536bcb2c4ac7d70c8ec665e", - "version" : "0.23.2" - } } ], "version" : 3 diff --git a/CodeEdit/Features/Documents/CodeFileDocument/CodeFileDocument.swift b/CodeEdit/Features/Documents/CodeFileDocument/CodeFileDocument.swift index 1409df2ed..b65f5a2b2 100644 --- a/CodeEdit/Features/Documents/CodeFileDocument/CodeFileDocument.swift +++ b/CodeEdit/Features/Documents/CodeFileDocument/CodeFileDocument.swift @@ -54,7 +54,7 @@ final class CodeFileDocument: NSDocument, ObservableObject { @Published var lspCoordinator: LSPContentCoordinator? /// Set by ``LanguageServer`` when initialized. - @Published var lspHighlightProvider: SemanticTokenHighlightProvider? + @Published var lspHighlightProvider: SemanticTokenHighlightProvider? /// Used to override detected languages. @Published var language: CodeLanguage? diff --git a/CodeEdit/Features/LSP/Editor/LSPContentCoordinator.swift b/CodeEdit/Features/LSP/Features/DocumentSync/LSPContentCoordinator.swift similarity index 100% rename from CodeEdit/Features/LSP/Editor/LSPContentCoordinator.swift rename to CodeEdit/Features/LSP/Features/DocumentSync/LSPContentCoordinator.swift diff --git a/CodeEdit/Features/LSP/Editor/SemanticTokenHighlightProvider.swift b/CodeEdit/Features/LSP/Features/SemanticTokens/SemanticTokenHighlightProvider.swift similarity index 98% rename from CodeEdit/Features/LSP/Editor/SemanticTokenHighlightProvider.swift rename to CodeEdit/Features/LSP/Features/SemanticTokens/SemanticTokenHighlightProvider.swift index 6a551b139..761bfd6e8 100644 --- a/CodeEdit/Features/LSP/Editor/SemanticTokenHighlightProvider.swift +++ b/CodeEdit/Features/LSP/Features/SemanticTokens/SemanticTokenHighlightProvider.swift @@ -21,7 +21,7 @@ import CodeEditLanguages /// ``SemanticTokenHighlightProvider/applyEdit(textView:range:delta:completion:)`` method. One might expect this class /// to respond to that method immediately, but it does not. It instead stores the completion passed in that method until /// it can respond to the edit with invalidated indices. -final class SemanticTokenHighlightProvider: HighlightProviding { +final class SemanticTokenHighlightProvider: HighlightProviding { enum HighlightError: Error { case lspRangeFailure } diff --git a/CodeEdit/Features/LSP/Editor/SemanticTokenMap.swift b/CodeEdit/Features/LSP/Features/SemanticTokens/SemanticTokenMap.swift similarity index 100% rename from CodeEdit/Features/LSP/Editor/SemanticTokenMap.swift rename to CodeEdit/Features/LSP/Features/SemanticTokens/SemanticTokenMap.swift diff --git a/CodeEdit/Features/LSP/Editor/SemanticTokenMapRangeProvider.swift b/CodeEdit/Features/LSP/Features/SemanticTokens/SemanticTokenMapRangeProvider.swift similarity index 100% rename from CodeEdit/Features/LSP/Editor/SemanticTokenMapRangeProvider.swift rename to CodeEdit/Features/LSP/Features/SemanticTokens/SemanticTokenMapRangeProvider.swift diff --git a/CodeEdit/Features/LSP/Editor/SemanticTokenStorage/SemanticTokenStorage.swift b/CodeEdit/Features/LSP/Features/SemanticTokens/SemanticTokenStorage/GenericSemanticTokenStorage.swift similarity index 82% rename from CodeEdit/Features/LSP/Editor/SemanticTokenStorage/SemanticTokenStorage.swift rename to CodeEdit/Features/LSP/Features/SemanticTokens/SemanticTokenStorage/GenericSemanticTokenStorage.swift index 876ab5cf1..ba4536911 100644 --- a/CodeEdit/Features/LSP/Editor/SemanticTokenStorage/SemanticTokenStorage.swift +++ b/CodeEdit/Features/LSP/Features/SemanticTokens/SemanticTokenStorage/GenericSemanticTokenStorage.swift @@ -1,5 +1,5 @@ // -// SemanticTokenStorage.swift +// GenericSemanticTokenStorage.swift // CodeEdit // // Created by Khan Winter on 12/26/24. @@ -12,8 +12,8 @@ import CodeEditSourceEditor /// Defines a protocol for an object to provide a storage mechanism for semantic tokens. /// /// There is only one concrete type that conforms to this in CE, but this protocol is useful in testing. -/// See ``LSPSemanticTokenStorage`` for use. -protocol SemanticTokenStorage: AnyObject { +/// See ``SemanticTokenStorage`` for use. +protocol GenericSemanticTokenStorage: AnyObject { var lastResultId: String? { get } var hasTokens: Bool { get } diff --git a/CodeEdit/Features/LSP/Editor/SemanticTokenStorage/SemanticTokenRange.swift b/CodeEdit/Features/LSP/Features/SemanticTokens/SemanticTokenStorage/SemanticTokenRange.swift similarity index 100% rename from CodeEdit/Features/LSP/Editor/SemanticTokenStorage/SemanticTokenRange.swift rename to CodeEdit/Features/LSP/Features/SemanticTokens/SemanticTokenStorage/SemanticTokenRange.swift diff --git a/CodeEdit/Features/LSP/Editor/SemanticTokenStorage/LSPSemanticTokenStorage.swift b/CodeEdit/Features/LSP/Features/SemanticTokens/SemanticTokenStorage/SemanticTokenStorage.swift similarity index 75% rename from CodeEdit/Features/LSP/Editor/SemanticTokenStorage/LSPSemanticTokenStorage.swift rename to CodeEdit/Features/LSP/Features/SemanticTokens/SemanticTokenStorage/SemanticTokenStorage.swift index 94d102018..01ae7dfcc 100644 --- a/CodeEdit/Features/LSP/Editor/SemanticTokenStorage/LSPSemanticTokenStorage.swift +++ b/CodeEdit/Features/LSP/Features/SemanticTokens/SemanticTokenStorage/SemanticTokenStorage.swift @@ -1,5 +1,5 @@ // -// ConcreteSemanticTokenStorage.swift +// SemanticTokenStorage.swift // CodeEdit // // Created by Khan Winter on 12/26/24. @@ -16,7 +16,7 @@ import CodeEditSourceEditor /// tokens and their decoded counterparts. It supports applying delta updates from the language server. /// /// See ``SemanticTokenHighlightProvider`` for it's connection to the editor view. -final class LSPSemanticTokenStorage: SemanticTokenStorage { +final class SemanticTokenStorage: GenericSemanticTokenStorage { /// Represents compressed semantic token data received from a language server. struct CurrentState { let resultId: String? @@ -46,11 +46,14 @@ final class LSPSemanticTokenStorage: SemanticTokenStorage { } var tokens: [SemanticToken] = [] -// var idx = findLowerBound(of: range.start, data: state.tokens[...]) -// while idx < state.tokens.count && state.tokens[idx].startPosition < range.end { -// tokens.append(state.tokens[idx]) -// idx += 1 -// } + guard var idx = findLowerBound(in: range, data: state.tokens[...]) else { + return [] + } + + while idx < state.tokens.count && state.tokens[idx].startPosition < range.end { + tokens.append(state.tokens[idx]) + idx += 1 + } return tokens } @@ -134,26 +137,33 @@ final class LSPSemanticTokenStorage: SemanticTokenStorage { // MARK: - Binary Search - /// Perform a binary search to find the given position - /// - Complexity: O(log n) + /// Finds the lowest index of a `SemanticToken` that is entirely within the specified range. + /// - Complexity: Runs an **O(log n)** binary search on the data array. + /// - Parameters: + /// - range: The range to search in, *not* inclusive. + /// - data: The tokens to search. Takes an array slice to avoid unnecessary copying. This must be ordered by + /// `startPosition`. + /// - Returns: The index in the data array of the lowest data element that lies within the given range, or `nil` + /// if none are found. func findLowerBound(in range: LSPRange, data: ArraySlice) -> Int? { - // TODO: This needs to find the closest value in a range, there's a good chance there's no result for a - // specific indice -// var lower = 0 -// var upper = data.count -// var idx = 0 -// while lower < upper { -// idx = lower + upper / 2 -// if data[idx].startPosition < position { -// lower = idx + 1 -// } else if data[idx].startPosition > position { -// upper = idx -// } else { -// return idx -// } -// } -// -// return (data[idx].startPosition..= range.start. + while low < high { + let mid = low + (high - low) / 2 + if data[mid].startPosition < range.start { + low = mid + 1 + } else { + high = mid + } + } + + // Return the item at `low` if it's valid. + if low < data.count && data[low].startPosition >= range.start && data[low].endPosition < range.end { + return low + } + return nil } } diff --git a/CodeEdit/Features/LSP/LanguageServer/LanguageServerFileMap.swift b/CodeEdit/Features/LSP/LanguageServer/LanguageServerFileMap.swift index eaf66b507..d90dbf52a 100644 --- a/CodeEdit/Features/LSP/LanguageServer/LanguageServerFileMap.swift +++ b/CodeEdit/Features/LSP/LanguageServer/LanguageServerFileMap.swift @@ -15,7 +15,7 @@ class LanguageServerFileMap { let uri: String var documentVersion: Int var contentCoordinator: LSPContentCoordinator - var semanticHighlighter: SemanticTokenHighlightProvider? + var semanticHighlighter: SemanticTokenHighlightProvider? } private var trackedDocuments: NSMapTable @@ -98,7 +98,7 @@ class LanguageServerFileMap { func semanticHighlighter( for document: CodeFileDocument - ) -> SemanticTokenHighlightProvider? { + ) -> SemanticTokenHighlightProvider? { guard let uri = document.languageServerURI else { return nil } return trackedDocumentData[uri]?.semanticHighlighter } From 5587a83c9d570f10755b2a3b5f5e074756a826a6 Mon Sep 17 00:00:00 2001 From: Khan Winter <35942988+thecoolwinter@users.noreply.github.com> Date: Wed, 12 Feb 2025 16:51:55 -0600 Subject: [PATCH 07/18] Achieve Generics Nirvana --- CodeEdit.xcodeproj/project.pbxproj | 14 ++++---- .../xcshareddata/swiftpm/Package.resolved | 2 +- .../CodeFileDocument/CodeFileDocument.swift | 23 +++++++----- .../Features/Editor/Views/CodeFileView.swift | 6 ++-- .../DocumentSync/LSPContentCoordinator.swift | 6 ++-- .../SemanticTokenHighlightProvider.swift | 13 ++++--- .../LanguageServer+DocumentSync.swift | 12 +++---- .../LSP/LanguageServer/LanguageServer.swift | 8 +++-- .../LanguageServerFileMap.swift | 36 ++++++++++--------- .../Features/LSP/LanguageServerDocument.swift | 23 ++++++++++++ .../Features/LSP/Service/LSPService.swift | 10 +++--- 11 files changed, 94 insertions(+), 59 deletions(-) create mode 100644 CodeEdit/Features/LSP/LanguageServerDocument.swift diff --git a/CodeEdit.xcodeproj/project.pbxproj b/CodeEdit.xcodeproj/project.pbxproj index bd67aaa27..6f1471324 100644 --- a/CodeEdit.xcodeproj/project.pbxproj +++ b/CodeEdit.xcodeproj/project.pbxproj @@ -373,6 +373,8 @@ 6C14CEB028777D3C001468FE /* FindNavigatorListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C14CEAF28777D3C001468FE /* FindNavigatorListViewController.swift */; }; 6C14CEB32877A68F001468FE /* FindNavigatorMatchListCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C14CEB22877A68F001468FE /* FindNavigatorMatchListCell.swift */; }; 6C18620A298BF5A800C663EA /* RecentProjectsListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C186209298BF5A800C663EA /* RecentProjectsListView.swift */; }; + 6C1A7E942D5D557D001B951C /* CodeEditSourceEditor in Frameworks */ = {isa = PBXBuildFile; productRef = 6C5246732D1E612700F57F11 /* CodeEditSourceEditor */; }; + 6C1A7E962D5D5762001B951C /* LanguageServerDocument.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C1A7E952D5D5762001B951C /* LanguageServerDocument.swift */; }; 6C1CC9982B1E770B0002349B /* AsyncFileIterator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C1CC9972B1E770B0002349B /* AsyncFileIterator.swift */; }; 6C1CC99B2B1E7CBC0002349B /* FindNavigatorIndexBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C1CC99A2B1E7CBC0002349B /* FindNavigatorIndexBar.swift */; }; 6C1F3DA22C18C55800F6DEF6 /* ShellIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C1F3DA12C18C55800F6DEF6 /* ShellIntegrationTests.swift */; }; @@ -408,7 +410,6 @@ 6C52466B2D1E507500F57F11 /* SemanticTokenStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C52466A2D1E506C00F57F11 /* SemanticTokenStorageTests.swift */; }; 6C52466D2D1E515700F57F11 /* SemanticToken+Position.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C52466C2D1E515700F57F11 /* SemanticToken+Position.swift */; }; 6C5246702D1E5CC100F57F11 /* SemanticTokenRange.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C52466F2D1E5CC100F57F11 /* SemanticTokenRange.swift */; }; - 6C5246742D1E612700F57F11 /* CodeEditSourceEditor in Frameworks */ = {isa = PBXBuildFile; productRef = 6C5246732D1E612700F57F11 /* CodeEditSourceEditor */; }; 6C53AAD829A6C4FD00EE9ED6 /* SplitView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C53AAD729A6C4FD00EE9ED6 /* SplitView.swift */; }; 6C578D8129CD294800DC73B2 /* ExtensionActivatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C578D8029CD294800DC73B2 /* ExtensionActivatorView.swift */; }; 6C578D8429CD343800DC73B2 /* ExtensionDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C578D8329CD343800DC73B2 /* ExtensionDetailView.swift */; }; @@ -431,7 +432,6 @@ 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 */; }; - 6C73A6D32D4F1E550012D95C /* CodeEditSourceEditor in Frameworks */ = {isa = PBXBuildFile; productRef = 6C73A6D22D4F1E550012D95C /* CodeEditSourceEditor */; }; 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 */; }; @@ -1083,6 +1083,7 @@ 6C14CEAF28777D3C001468FE /* FindNavigatorListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FindNavigatorListViewController.swift; sourceTree = ""; }; 6C14CEB22877A68F001468FE /* FindNavigatorMatchListCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FindNavigatorMatchListCell.swift; sourceTree = ""; }; 6C186209298BF5A800C663EA /* RecentProjectsListView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecentProjectsListView.swift; sourceTree = ""; }; + 6C1A7E952D5D5762001B951C /* LanguageServerDocument.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LanguageServerDocument.swift; sourceTree = ""; }; 6C1CC9972B1E770B0002349B /* AsyncFileIterator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncFileIterator.swift; sourceTree = ""; }; 6C1CC99A2B1E7CBC0002349B /* FindNavigatorIndexBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FindNavigatorIndexBar.swift; sourceTree = ""; }; 6C1F3DA12C18C55800F6DEF6 /* ShellIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShellIntegrationTests.swift; sourceTree = ""; }; @@ -1365,6 +1366,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 6C1A7E942D5D557D001B951C /* CodeEditSourceEditor in Frameworks */, 6C85BB402C2105ED00EB5DEF /* CodeEditKit in Frameworks */, 6C66C31329D05CDC00DE9ED2 /* GRDB in Frameworks */, 58F2EB1E292FB954004A9BDE /* Sparkle in Frameworks */, @@ -1379,7 +1381,6 @@ 6C6BD6F429CD142C00235D17 /* CollectionConcurrencyKit in Frameworks */, 6C85BB442C210EFD00EB5DEF /* SwiftUIIntrospect in Frameworks */, 6CB446402B6DFF3A00539ED0 /* CodeEditSourceEditor in Frameworks */, - 6C9AE6992D1DD84600FAE8D2 /* CodeEditSourceEditor in Frameworks */, 2816F594280CF50500DD548B /* CodeEditSymbols in Frameworks */, 30CB64942C16CA9100CC8A9E /* LanguageClient in Frameworks */, 6C6BD6F829CD14D100235D17 /* CodeEditKit in Frameworks */, @@ -1630,6 +1631,7 @@ 30B087FB2C0D53080063A882 /* LSP */ = { isa = PBXGroup; children = ( + 6C1A7E952D5D5762001B951C /* LanguageServerDocument.swift */, 30B087FA2C0D53080063A882 /* LSPUtil.swift */, 6C1A7E932D5D508A001B951C /* Features */, 6CD26C732C8EA71F00ADBA38 /* LanguageServer */, @@ -3878,7 +3880,6 @@ 6CD3CA542C8B508200D83DCD /* CodeEditSourceEditor */, 6CB94D022CA1205100E8651C /* AsyncAlgorithms */, 6CC00A8A2CBEF150004E8134 /* CodeEditSourceEditor */, - 6C9AE6982D1DD84600FAE8D2 /* CodeEditSourceEditor */, 6C5246732D1E612700F57F11 /* CodeEditSourceEditor */, ); productName = CodeEdit; @@ -4633,6 +4634,7 @@ 58822525292C280D00E83CDE /* StatusBarMenuStyle.swift in Sources */, 6C147C4229A328C10089B630 /* Editor.swift in Sources */, B6C4F2A32B3CA74800B2B140 /* CommitDetailsView.swift in Sources */, + 6C1A7E962D5D5762001B951C /* LanguageServerDocument.swift in Sources */, 6C2C155829B4F49100EA60A5 /* SplitViewItem.swift in Sources */, 6CDA84AD284C1BA000C1CC3A /* EditorTabBarContextMenu.swift in Sources */, 6C81916729B3E80700B75C92 /* ModifierKeysObserver.swift in Sources */, @@ -5983,10 +5985,6 @@ isa = XCSwiftPackageProductDependency; productName = CodeEditKit; }; - 6C73A6D22D4F1E550012D95C /* CodeEditSourceEditor */ = { - isa = XCSwiftPackageProductDependency; - productName = CodeEditSourceEditor; - }; 6C7B1C752A1D57CE005CBBFC /* SwiftLint */ = { isa = XCSwiftPackageProductDependency; package = 287136B1292A407E00E9F5F4 /* XCRemoteSwiftPackageReference "SwiftLintPlugin" */; diff --git a/CodeEdit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/CodeEdit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 8440b8a74..36e0897c9 100644 --- a/CodeEdit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/CodeEdit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "aef43d6aa0c467418565c574c33495a50d6e24057eb350c17704ab4ae2aead6c", + "originHash" : "ac57a6899925c3e4ac6d43aed791c845c6fc24a4441b6a10297a207d951b7836", "pins" : [ { "identity" : "anycodable", diff --git a/CodeEdit/Features/Documents/CodeFileDocument/CodeFileDocument.swift b/CodeEdit/Features/Documents/CodeFileDocument/CodeFileDocument.swift index b65f5a2b2..9fd315875 100644 --- a/CodeEdit/Features/Documents/CodeFileDocument/CodeFileDocument.swift +++ b/CodeEdit/Features/Documents/CodeFileDocument/CodeFileDocument.swift @@ -50,12 +50,6 @@ final class CodeFileDocument: NSDocument, ObservableObject { /// See ``CodeEditSourceEditor/CombineCoordinator``. @Published var contentCoordinator: CombineCoordinator = CombineCoordinator() - /// Set by ``LanguageServer`` when initialized. - @Published var lspCoordinator: LSPContentCoordinator? - - /// Set by ``LanguageServer`` when initialized. - @Published var lspHighlightProvider: SemanticTokenHighlightProvider? - /// Used to override detected languages. @Published var language: CodeLanguage? @@ -68,6 +62,9 @@ final class CodeFileDocument: NSDocument, ObservableObject { /// Document-specific overridden line wrap preference. @Published var wrapLines: Bool? + /// Set up by ``LanguageServer``, conforms this type to ``LanguageServerDocument``. + @Published var languageServerObjects: LanguageServerDocumentObjects = .init() + /// The type of data this file document contains. /// /// If its text content is not nil, a `text` UTType is returned. @@ -86,9 +83,6 @@ final class CodeFileDocument: NSDocument, ObservableObject { return type } - /// A stable string to use when identifying documents with language servers. - var languageServerURI: String? { fileURL?.absolutePath } - /// Specify options for opening the file such as the initial cursor positions. /// Nulled by ``CodeFileView`` on first load. var openOptions: OpenOptions? @@ -194,6 +188,10 @@ final class CodeFileDocument: NSDocument, ObservableObject { NotificationCenter.default.post(name: Self.didCloseNotification, object: fileURL) } + /// Determines the code language of the document. + /// Use ``CodeFileDocument/language`` for the default value before using this. That property is used to override + /// the file's language. + /// - Returns: The detected code language. func getLanguage() -> CodeLanguage { guard let url = fileURL else { return .default @@ -209,3 +207,10 @@ final class CodeFileDocument: NSDocument, ObservableObject { fileURL?.findWorkspace() } } + +// MARK: LanguageServerDocument + +extension CodeFileDocument: LanguageServerDocument { + /// A stable string to use when identifying documents with language servers. + var languageServerURI: String? { fileURL?.absolutePath } +} diff --git a/CodeEdit/Features/Editor/Views/CodeFileView.swift b/CodeEdit/Features/Editor/Views/CodeFileView.swift index 7b675be4d..d70a8c733 100644 --- a/CodeEdit/Features/Editor/Views/CodeFileView.swift +++ b/CodeEdit/Features/Editor/Views/CodeFileView.swift @@ -63,7 +63,7 @@ struct CodeFileView: View { self.textViewCoordinators = textViewCoordinators + [codeFile.contentCoordinator] - + [codeFile.lspCoordinator].compactMap({ $0 }) + + [codeFile.languageServerObjects.textCoordinator].compactMap({ $0 }) self.isEditable = isEditable if let openOptions = codeFile.openOptions { @@ -162,8 +162,8 @@ struct CodeFileView: View { .onChange(of: bracketHighlight) { _ in bracketPairHighlight = getBracketPairHighlight() } - .onReceive(codeFile.$lspHighlightProvider) { provider in - updateHighlightProviders(provider) + .onReceive(codeFile.$languageServerObjects) { languageServerObjects in + updateHighlightProviders(languageServerObjects.highlightProvider) } } diff --git a/CodeEdit/Features/LSP/Features/DocumentSync/LSPContentCoordinator.swift b/CodeEdit/Features/LSP/Features/DocumentSync/LSPContentCoordinator.swift index 010e6c583..28fbb4a5b 100644 --- a/CodeEdit/Features/LSP/Features/DocumentSync/LSPContentCoordinator.swift +++ b/CodeEdit/Features/LSP/Features/DocumentSync/LSPContentCoordinator.swift @@ -19,7 +19,7 @@ import LanguageServerProtocol /// Language servers expect edits to be sent in chunks (and it helps reduce processing overhead). To do this, this class /// keeps an async stream around for the duration of its lifetime. The stream is sent edit notifications, which are then /// chunked into 250ms timed groups before being sent to the ``LanguageServer``. -class LSPContentCoordinator: TextViewCoordinator, TextViewDelegate { +class LSPContentCoordinator: TextViewCoordinator, TextViewDelegate { // Required to avoid a large_tuple lint error private struct SequenceElement: Sendable { let uri: String @@ -32,11 +32,11 @@ class LSPContentCoordinator: TextViewCoordinator, TextViewDelegate { private var sequenceContinuation: AsyncStream.Continuation? private var task: Task? - weak var languageServer: LanguageServer? + weak var languageServer: LanguageServer? var documentURI: String /// Initializes a content coordinator, and begins an async stream of updates - init(documentURI: String, languageServer: LanguageServer) { + init(documentURI: String, languageServer: LanguageServer) { self.documentURI = documentURI self.languageServer = languageServer self.stream = AsyncStream { continuation in diff --git a/CodeEdit/Features/LSP/Features/SemanticTokens/SemanticTokenHighlightProvider.swift b/CodeEdit/Features/LSP/Features/SemanticTokens/SemanticTokenHighlightProvider.swift index 761bfd6e8..b685133e6 100644 --- a/CodeEdit/Features/LSP/Features/SemanticTokens/SemanticTokenHighlightProvider.swift +++ b/CodeEdit/Features/LSP/Features/SemanticTokens/SemanticTokenHighlightProvider.swift @@ -21,7 +21,10 @@ import CodeEditLanguages /// ``SemanticTokenHighlightProvider/applyEdit(textView:range:delta:completion:)`` method. One might expect this class /// to respond to that method immediately, but it does not. It instead stores the completion passed in that method until /// it can respond to the edit with invalidated indices. -final class SemanticTokenHighlightProvider: HighlightProviding { +final class SemanticTokenHighlightProvider< + Storage: GenericSemanticTokenStorage, + DocumentType: LanguageServerDocument +>: HighlightProviding { enum HighlightError: Error { case lspRangeFailure } @@ -31,7 +34,7 @@ final class SemanticTokenHighlightProvider private let tokenMap: SemanticTokenMap private let documentURI: String - private weak var languageServer: LanguageServer? + private weak var languageServer: LanguageServer? private weak var textView: TextView? private var lastEditCallback: EditCallback? @@ -42,7 +45,7 @@ final class SemanticTokenHighlightProvider textView?.documentRange ?? .zero } - init(tokenMap: SemanticTokenMap, languageServer: LanguageServer, documentURI: String) { + init(tokenMap: SemanticTokenMap, languageServer: LanguageServer, documentURI: String) { self.tokenMap = tokenMap self.languageServer = languageServer self.documentURI = documentURI @@ -88,7 +91,7 @@ final class SemanticTokenHighlightProvider /// Requests and applies a token delta. Requires a previous response identifier. private func requestDeltaTokens( - languageServer: LanguageServer, + languageServer: LanguageServer, textView: TextView, lastResultId: String ) async throws { @@ -108,7 +111,7 @@ final class SemanticTokenHighlightProvider /// Requests and applies tokens for an entire document. This does not require a previous response id, and should be /// used in place of `requestDeltaTokens` when that's the case. - private func requestTokens(languageServer: LanguageServer, textView: TextView) async throws { + private func requestTokens(languageServer: LanguageServer, textView: TextView) async throws { guard let response = try await languageServer.requestSemanticTokens(for: documentURI) else { return } diff --git a/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+DocumentSync.swift b/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+DocumentSync.swift index 5beb130f1..ba23d1f37 100644 --- a/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+DocumentSync.swift +++ b/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+DocumentSync.swift @@ -12,7 +12,7 @@ extension LanguageServer { /// 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 { + func openDocument(_ document: DocumentType) async throws { do { guard resolveOpenCloseSupport(), let content = await getIsolatedDocumentContent(document) else { return @@ -104,7 +104,7 @@ extension LanguageServer { // Let the semantic token provider know about the update. // Note for future: If a related LSP object need notifying about document changes, do it here. - try await document.lspHighlightProvider?.documentDidChange() + try await document.languageServerObjects.highlightProvider?.documentDidChange() } catch { logger.warning("closeDocument: Error \(error)") throw error @@ -115,7 +115,7 @@ extension LanguageServer { /// Helper function for grabbing a document's content from the main actor. @MainActor - private func getIsolatedDocumentContent(_ document: CodeFileDocument) -> DocumentContent? { + private func getIsolatedDocumentContent(_ document: DocumentType) -> DocumentContent? { guard let uri = document.languageServerURI, let content = document.content?.string else { return nil @@ -124,9 +124,9 @@ extension LanguageServer { } @MainActor - private func updateIsolatedDocument(_ document: CodeFileDocument) { - document.lspCoordinator = openFiles.contentCoordinator(for: document) - document.lspHighlightProvider = openFiles.semanticHighlighter(for: document) + private func updateIsolatedDocument(_ document: DocumentType) { + document.languageServerObjects.textCoordinator = openFiles.contentCoordinator(for: document) + document.languageServerObjects.highlightProvider = openFiles.semanticHighlighter(for: document) } // swiftlint:disable line_length diff --git a/CodeEdit/Features/LSP/LanguageServer/LanguageServer.swift b/CodeEdit/Features/LSP/LanguageServer/LanguageServer.swift index 53fcaf2e3..ef37c8896 100644 --- a/CodeEdit/Features/LSP/LanguageServer/LanguageServer.swift +++ b/CodeEdit/Features/LSP/LanguageServer/LanguageServer.swift @@ -11,8 +11,10 @@ import LanguageClient import LanguageServerProtocol import OSLog -class LanguageServer { - static let logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "", category: "LanguageServer") +class LanguageServer { + static var logger: Logger { + Logger(subsystem: Bundle.main.bundleIdentifier ?? "", category: "LanguageServer") + } let logger: Logger /// Identifies which language the server belongs to @@ -25,7 +27,7 @@ class LanguageServer { /// 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 + let openFiles: LanguageServerFileMap /// Maps the language server's highlight config to one CodeEdit can read. See ``SemanticTokenMap``. let highlightMap: SemanticTokenMap? diff --git a/CodeEdit/Features/LSP/LanguageServer/LanguageServerFileMap.swift b/CodeEdit/Features/LSP/LanguageServer/LanguageServerFileMap.swift index d90dbf52a..642d21703 100644 --- a/CodeEdit/Features/LSP/LanguageServer/LanguageServerFileMap.swift +++ b/CodeEdit/Features/LSP/LanguageServer/LanguageServerFileMap.swift @@ -9,25 +9,27 @@ import Foundation import LanguageServerProtocol /// Tracks data associated with files and language servers. -class LanguageServerFileMap { +class LanguageServerFileMap { + typealias HighlightProviderType = SemanticTokenHighlightProvider + /// Extend this struct as more objects are associated with a code document. private struct DocumentObject { let uri: String var documentVersion: Int - var contentCoordinator: LSPContentCoordinator - var semanticHighlighter: SemanticTokenHighlightProvider? + var contentCoordinator: LSPContentCoordinator + var semanticHighlighter: HighlightProviderType? } - private var trackedDocuments: NSMapTable + private var trackedDocuments: NSMapTable private var trackedDocumentData: [String: DocumentObject] = [:] init() { - trackedDocuments = NSMapTable(valueOptions: [.weakMemory]) + trackedDocuments = NSMapTable(valueOptions: [.weakMemory]) } // MARK: - Track & Remove Documents - func addDocument(_ document: CodeFileDocument, for server: LanguageServer) { + func addDocument(_ document: DocumentType, for server: LanguageServer) { guard let uri = document.languageServerURI else { return } trackedDocuments.setObject(document, forKey: uri as NSString) var docData = DocumentObject( @@ -41,18 +43,22 @@ class LanguageServerFileMap { ) if let tokenMap = server.highlightMap { - docData.semanticHighlighter = .init(tokenMap: tokenMap, languageServer: server, documentURI: uri) + docData.semanticHighlighter = HighlightProviderType( + tokenMap: tokenMap, + languageServer: server, + documentURI: uri + ) } trackedDocumentData[uri] = docData } - func document(for uri: DocumentUri) -> CodeFileDocument? { + func document(for uri: DocumentUri) -> DocumentType? { let url = URL(filePath: uri) return trackedDocuments.object(forKey: url.absolutePath as NSString) } - func removeDocument(for document: CodeFileDocument) { + func removeDocument(for document: DocumentType) { guard let uri = document.languageServerURI else { return } removeDocument(for: uri) } @@ -64,7 +70,7 @@ class LanguageServerFileMap { // MARK: - Version Number Tracking - func incrementVersion(for document: CodeFileDocument) -> Int { + func incrementVersion(for document: DocumentType) -> Int { guard let uri = document.languageServerURI else { return 0 } return incrementVersion(for: uri) } @@ -74,7 +80,7 @@ class LanguageServerFileMap { return trackedDocumentData[uri]?.documentVersion ?? 0 } - func documentVersion(for document: CodeFileDocument) -> Int? { + func documentVersion(for document: DocumentType) -> Int? { guard let uri = document.languageServerURI else { return nil } return documentVersion(for: uri) } @@ -85,20 +91,18 @@ class LanguageServerFileMap { // MARK: - Content Coordinator - func contentCoordinator(for document: CodeFileDocument) -> LSPContentCoordinator? { + func contentCoordinator(for document: DocumentType) -> LSPContentCoordinator? { guard let uri = document.languageServerURI else { return nil } return contentCoordinator(for: uri) } - func contentCoordinator(for uri: DocumentUri) -> LSPContentCoordinator? { + func contentCoordinator(for uri: DocumentUri) -> LSPContentCoordinator? { trackedDocumentData[uri]?.contentCoordinator } // MARK: - Semantic Highlighter - func semanticHighlighter( - for document: CodeFileDocument - ) -> SemanticTokenHighlightProvider? { + func semanticHighlighter(for document: DocumentType) -> HighlightProviderType? { guard let uri = document.languageServerURI else { return nil } return trackedDocumentData[uri]?.semanticHighlighter } diff --git a/CodeEdit/Features/LSP/LanguageServerDocument.swift b/CodeEdit/Features/LSP/LanguageServerDocument.swift new file mode 100644 index 000000000..8b4b09a47 --- /dev/null +++ b/CodeEdit/Features/LSP/LanguageServerDocument.swift @@ -0,0 +1,23 @@ +// +// LanguageServerDocument.swift +// CodeEdit +// +// Created by Khan Winter on 2/12/25. +// + +import AppKit +import CodeEditLanguages + +/// A set of properties a language server sets when a document is registered. +struct LanguageServerDocumentObjects { + var textCoordinator: LSPContentCoordinator? + var highlightProvider: SemanticTokenHighlightProvider? +} + +/// A protocol that allows a language server to register objects on a text document. +protocol LanguageServerDocument: AnyObject { + var content: NSTextStorage? { get } + var languageServerURI: String? { get } + var languageServerObjects: LanguageServerDocumentObjects { get set } + func getLanguage() -> CodeLanguage +} diff --git a/CodeEdit/Features/LSP/Service/LSPService.swift b/CodeEdit/Features/LSP/Service/LSPService.swift index 5110c6c3e..99366f3b3 100644 --- a/CodeEdit/Features/LSP/Service/LSPService.swift +++ b/CodeEdit/Features/LSP/Service/LSPService.swift @@ -112,7 +112,7 @@ final class LSPService: ObservableObject { } /// Holds the active language clients - var languageClients: [ClientKey: LanguageServer] = [:] + 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 @@ -162,7 +162,7 @@ final class LSPService: ObservableObject { } /// Gets the language client for the specified language - func languageClient(for languageId: LanguageIdentifier, workspacePath: String) -> LanguageServer? { + func languageClient(for languageId: LanguageIdentifier, workspacePath: String) -> LanguageServer? { return languageClients[ClientKey(languageId, workspacePath)] } @@ -174,14 +174,14 @@ final class LSPService: ObservableObject { func startServer( for languageId: LanguageIdentifier, workspacePath: String - ) async throws -> LanguageServer { + ) 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( + let server = try await LanguageServer.createServer( for: languageId, with: serverBinary, workspacePath: workspacePath @@ -203,7 +203,7 @@ final class LSPService: ObservableObject { return } Task { - let languageServer: LanguageServer + let languageServer: LanguageServer do { if let server = self.languageClients[ClientKey(lspLanguage, workspacePath)] { languageServer = server From 9935aaeaf012edc61538c526a03560315fa6db33 Mon Sep 17 00:00:00 2001 From: Khan Winter <35942988+thecoolwinter@users.noreply.github.com> Date: Wed, 12 Feb 2025 17:34:02 -0600 Subject: [PATCH 08/18] Test Specialized Document Type --- CodeEdit.xcodeproj/project.pbxproj | 24 ++++-- .../LanguageServer+DocumentSync.swift | 16 +++- ... => LanguageServer+CodeFileDocument.swift} | 20 ++--- .../LSP/LanguageServer+DocumentObjects.swift | 80 +++++++++++++++++++ .../SemanticTokenMapTests.swift | 0 .../SemanticTokenStorageTests.swift | 6 +- 6 files changed, 125 insertions(+), 21 deletions(-) rename CodeEditTests/Features/LSP/{LanguageServer+DocumentTests.swift => LanguageServer+CodeFileDocument.swift} (93%) create mode 100644 CodeEditTests/Features/LSP/LanguageServer+DocumentObjects.swift rename CodeEditTests/Features/LSP/{ => SemanticTokens}/SemanticTokenMapTests.swift (100%) rename CodeEditTests/Features/LSP/{ => SemanticTokens}/SemanticTokenStorageTests.swift (75%) diff --git a/CodeEdit.xcodeproj/project.pbxproj b/CodeEdit.xcodeproj/project.pbxproj index 6f1471324..2d4914532 100644 --- a/CodeEdit.xcodeproj/project.pbxproj +++ b/CodeEdit.xcodeproj/project.pbxproj @@ -375,6 +375,7 @@ 6C18620A298BF5A800C663EA /* RecentProjectsListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C186209298BF5A800C663EA /* RecentProjectsListView.swift */; }; 6C1A7E942D5D557D001B951C /* CodeEditSourceEditor in Frameworks */ = {isa = PBXBuildFile; productRef = 6C5246732D1E612700F57F11 /* CodeEditSourceEditor */; }; 6C1A7E962D5D5762001B951C /* LanguageServerDocument.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C1A7E952D5D5762001B951C /* LanguageServerDocument.swift */; }; + 6C1A7E9B2D5D5E0E001B951C /* LanguageServer+DocumentObjects.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C1A7E9A2D5D5E0E001B951C /* LanguageServer+DocumentObjects.swift */; }; 6C1CC9982B1E770B0002349B /* AsyncFileIterator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C1CC9972B1E770B0002349B /* AsyncFileIterator.swift */; }; 6C1CC99B2B1E7CBC0002349B /* FindNavigatorIndexBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C1CC99A2B1E7CBC0002349B /* FindNavigatorIndexBar.swift */; }; 6C1F3DA22C18C55800F6DEF6 /* ShellIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C1F3DA12C18C55800F6DEF6 /* ShellIntegrationTests.swift */; }; @@ -485,7 +486,7 @@ 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 */; }; - 6CD26C8A2C8F91ED00ADBA38 /* LanguageServer+DocumentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CD26C892C8F91ED00ADBA38 /* LanguageServer+DocumentTests.swift */; }; + 6CD26C8A2C8F91ED00ADBA38 /* LanguageServer+CodeFileDocument.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CD26C892C8F91ED00ADBA38 /* LanguageServer+CodeFileDocument.swift */; }; 6CD3CA552C8B508200D83DCD /* CodeEditSourceEditor in Frameworks */ = {isa = PBXBuildFile; productRef = 6CD3CA542C8B508200D83DCD /* CodeEditSourceEditor */; }; 6CDA84AD284C1BA000C1CC3A /* EditorTabBarContextMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CDA84AC284C1BA000C1CC3A /* EditorTabBarContextMenu.swift */; }; 6CDAFDDD2D35B2A0002B2D47 /* CEWorkspaceFileManager+Error.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CDAFDDC2D35B2A0002B2D47 /* CEWorkspaceFileManager+Error.swift */; }; @@ -1084,6 +1085,7 @@ 6C14CEB22877A68F001468FE /* FindNavigatorMatchListCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FindNavigatorMatchListCell.swift; sourceTree = ""; }; 6C186209298BF5A800C663EA /* RecentProjectsListView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecentProjectsListView.swift; sourceTree = ""; }; 6C1A7E952D5D5762001B951C /* LanguageServerDocument.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LanguageServerDocument.swift; sourceTree = ""; }; + 6C1A7E9A2D5D5E0E001B951C /* LanguageServer+DocumentObjects.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LanguageServer+DocumentObjects.swift"; sourceTree = ""; }; 6C1CC9972B1E770B0002349B /* AsyncFileIterator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncFileIterator.swift; sourceTree = ""; }; 6C1CC99A2B1E7CBC0002349B /* FindNavigatorIndexBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FindNavigatorIndexBar.swift; sourceTree = ""; }; 6C1F3DA12C18C55800F6DEF6 /* ShellIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShellIntegrationTests.swift; sourceTree = ""; }; @@ -1178,7 +1180,7 @@ 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 = ""; }; - 6CD26C892C8F91ED00ADBA38 /* LanguageServer+DocumentTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LanguageServer+DocumentTests.swift"; sourceTree = ""; }; + 6CD26C892C8F91ED00ADBA38 /* LanguageServer+CodeFileDocument.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LanguageServer+CodeFileDocument.swift"; sourceTree = ""; }; 6CDA84AC284C1BA000C1CC3A /* EditorTabBarContextMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditorTabBarContextMenu.swift; sourceTree = ""; }; 6CDAFDDC2D35B2A0002B2D47 /* CEWorkspaceFileManager+Error.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CEWorkspaceFileManager+Error.swift"; sourceTree = ""; }; 6CDAFDDE2D35DADD002B2D47 /* String+ValidFileName.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+ValidFileName.swift"; sourceTree = ""; }; @@ -2963,6 +2965,15 @@ path = Features; sourceTree = ""; }; + 6C1A7E992D5D5D9C001B951C /* SemanticTokens */ = { + isa = PBXGroup; + children = ( + 6C3B4CD32D0E2CB000C6759E /* SemanticTokenMapTests.swift */, + 6C52466A2D1E506C00F57F11 /* SemanticTokenStorageTests.swift */, + ); + path = SemanticTokens; + sourceTree = ""; + }; 6C2384302C796EBD003FBDD4 /* ChangedFile */ = { isa = PBXGroup; children = ( @@ -3234,9 +3245,9 @@ isa = PBXGroup; children = ( 6C7D6D452C9092EC00B69EE0 /* BufferingServerConnection.swift */, - 6CD26C892C8F91ED00ADBA38 /* LanguageServer+DocumentTests.swift */, - 6C3B4CD32D0E2CB000C6759E /* SemanticTokenMapTests.swift */, - 6C52466A2D1E506C00F57F11 /* SemanticTokenStorageTests.swift */, + 6CD26C892C8F91ED00ADBA38 /* LanguageServer+CodeFileDocument.swift */, + 6C1A7E9A2D5D5E0E001B951C /* LanguageServer+DocumentObjects.swift */, + 6C1A7E992D5D5D9C001B951C /* SemanticTokens */, ); path = LSP; sourceTree = ""; @@ -4684,7 +4695,7 @@ 587B61012934170A00D5CD8F /* UnitTests_Extensions.swift in Sources */, 6C1F3DA22C18C55800F6DEF6 /* ShellIntegrationTests.swift in Sources */, 283BDCC52972F236002AFF81 /* AcknowledgementsTests.swift in Sources */, - 6CD26C8A2C8F91ED00ADBA38 /* LanguageServer+DocumentTests.swift in Sources */, + 6CD26C8A2C8F91ED00ADBA38 /* LanguageServer+CodeFileDocument.swift in Sources */, 4EE96ECB2960565E00FFBEA8 /* DocumentsUnitTests.swift in Sources */, 4EE96ECE296059E000FFBEA8 /* NSHapticFeedbackPerformerMock.swift in Sources */, 61FB03AE2C3C2493001B3671 /* CEActiveTaskTests.swift in Sources */, @@ -4692,6 +4703,7 @@ 6195E3112B640485007261CA /* WorkspaceDocument+SearchState+IndexTests.swift in Sources */, 6130536B2B24722C00D767E3 /* AsyncIndexingTests.swift in Sources */, 613899C02B6E70FE00A5CAF6 /* FuzzySearchTests.swift in Sources */, + 6C1A7E9B2D5D5E0E001B951C /* LanguageServer+DocumentObjects.swift in Sources */, 6C3B4CD42D0E2CB000C6759E /* SemanticTokenMapTests.swift in Sources */, 6195E30D2B64044F007261CA /* WorkspaceDocument+SearchState+FindTests.swift in Sources */, 6195E30F2B640474007261CA /* WorkspaceDocument+SearchState+FindAndReplaceTests.swift in Sources */, diff --git a/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+DocumentSync.swift b/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+DocumentSync.swift index ba23d1f37..be69c6647 100644 --- a/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+DocumentSync.swift +++ b/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+DocumentSync.swift @@ -41,9 +41,12 @@ extension LanguageServer { /// - 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 } + guard resolveOpenCloseSupport(), let document = openFiles.document(for: uri) else { return } logger.debug("Closing document \(uri, privacy: .private)") + openFiles.removeDocument(for: uri) + await clearIsolatedDocument(document) + let params = DidCloseTextDocumentParams(textDocument: TextDocumentIdentifier(uri: uri)) try await lspInstance.textDocumentDidClose(params) } catch { @@ -125,8 +128,15 @@ extension LanguageServer { @MainActor private func updateIsolatedDocument(_ document: DocumentType) { - document.languageServerObjects.textCoordinator = openFiles.contentCoordinator(for: document) - document.languageServerObjects.highlightProvider = openFiles.semanticHighlighter(for: document) + document.languageServerObjects = LanguageServerDocumentObjects( + textCoordinator: openFiles.contentCoordinator(for: document), + highlightProvider: openFiles.semanticHighlighter(for: document) + ) + } + + @MainActor + private func clearIsolatedDocument(_ document: DocumentType) { + document.languageServerObjects = LanguageServerDocumentObjects() } // swiftlint:disable line_length diff --git a/CodeEditTests/Features/LSP/LanguageServer+DocumentTests.swift b/CodeEditTests/Features/LSP/LanguageServer+CodeFileDocument.swift similarity index 93% rename from CodeEditTests/Features/LSP/LanguageServer+DocumentTests.swift rename to CodeEditTests/Features/LSP/LanguageServer+CodeFileDocument.swift index e4a726b57..0cfa75a7d 100644 --- a/CodeEditTests/Features/LSP/LanguageServer+DocumentTests.swift +++ b/CodeEditTests/Features/LSP/LanguageServer+CodeFileDocument.swift @@ -1,5 +1,5 @@ // -// LanguageServer+DocumentTests.swift +// LanguageServer+CodeFileDocument.swift // CodeEditTests // // Created by Khan Winter on 9/9/24. @@ -13,10 +13,12 @@ import LanguageServerProtocol @testable import CodeEdit -final class LanguageServerDocumentTests: XCTestCase { +final class LanguageServerCodeFileDocumentTests: XCTestCase { // Test opening documents in CodeEdit triggers creating a language server, // further opened documents don't create new servers + typealias LanguageServerType = LanguageServer + var tempTestDir: URL! override func setUp() { @@ -44,7 +46,7 @@ final class LanguageServerDocumentTests: XCTestCase { } } - func makeTestServer() async throws -> (connection: BufferingServerConnection, server: LanguageServer) { + func makeTestServer() async throws -> (connection: BufferingServerConnection, server: LanguageServerType) { let bufferingConnection = BufferingServerConnection() var capabilities = ServerCapabilities() capabilities.textDocumentSync = .optionA( @@ -56,12 +58,12 @@ final class LanguageServerDocumentTests: XCTestCase { save: nil ) ) - let server = LanguageServer( + let server = LanguageServerType( languageId: .swift, binary: .init(execPath: "", args: [], env: nil), lspInstance: InitializingServer( server: bufferingConnection, - initializeParamsProvider: LanguageServer.getInitParams(workspacePath: tempTestDir.path()) + initializeParamsProvider: LanguageServerType.getInitParams(workspacePath: tempTestDir.path()) ), serverCapabilities: capabilities, rootPath: tempTestDir @@ -81,7 +83,7 @@ final class LanguageServerDocumentTests: XCTestCase { } func openCodeFile( - for server: LanguageServer, + for server: LanguageServerType, connection: BufferingServerConnection, file: CEWorkspaceFile, syncOption: TwoTypeOption? @@ -138,7 +140,7 @@ final class LanguageServerDocumentTests: XCTestCase { CodeEditDocumentController.shared.addDocument(workspace) // Add a CEWorkspaceFile - try fileManager.addFile(fileName: "example", toFile: fileManager.workspaceItem, useExtension: "swift") + _ = try fileManager.addFile(fileName: "example", toFile: fileManager.workspaceItem, useExtension: "swift") guard let file = fileManager.childrenOfFile(fileManager.workspaceItem)?.first else { XCTFail("No File") return @@ -201,7 +203,7 @@ final class LanguageServerDocumentTests: XCTestCase { let (_, fileManager) = try makeTestWorkspace() // Make our example file - try fileManager.addFile(fileName: "example", toFile: fileManager.workspaceItem, useExtension: "swift") + _ = try fileManager.addFile(fileName: "example", toFile: fileManager.workspaceItem, useExtension: "swift") guard let file = fileManager.childrenOfFile(fileManager.workspaceItem)?.first else { XCTFail("No File") return @@ -261,7 +263,7 @@ final class LanguageServerDocumentTests: XCTestCase { let (_, fileManager) = try makeTestWorkspace() // Make our example file - try fileManager.addFile(fileName: "example", toFile: fileManager.workspaceItem, useExtension: "swift") + _ = try fileManager.addFile(fileName: "example", toFile: fileManager.workspaceItem, useExtension: "swift") guard let file = fileManager.childrenOfFile(fileManager.workspaceItem)?.first else { XCTFail("No File") return diff --git a/CodeEditTests/Features/LSP/LanguageServer+DocumentObjects.swift b/CodeEditTests/Features/LSP/LanguageServer+DocumentObjects.swift new file mode 100644 index 000000000..76b2e8cf3 --- /dev/null +++ b/CodeEditTests/Features/LSP/LanguageServer+DocumentObjects.swift @@ -0,0 +1,80 @@ +// +// LanguageServer+DocumentObjects.swift +// CodeEditTests +// +// Created by Khan Winter on 2/12/25. +// + +import XCTest +import CodeEditTextView +import CodeEditSourceEditor +import CodeEditLanguages +import LanguageClient +import LanguageServerProtocol + +@testable import CodeEdit + +final class LanguageServerDocumentObjectsTests: XCTestCase { + final class MockDocumentType: LanguageServerDocument { + var content: NSTextStorage? + var languageServerURI: String? + var languageServerObjects: LanguageServerDocumentObjects + + init() { + self.content = NSTextStorage(string: "hello world") + self.languageServerURI = "/test/file/path" + self.languageServerObjects = .init() + } + + func getLanguage() -> CodeLanguage { + .swift + } + } + + typealias LanguageServerType = LanguageServer + + var document: MockDocumentType! + var server: LanguageServerType! + + // MARK: - Set Up + + override func setUp() async throws { + var capabilities = ServerCapabilities() + capabilities.textDocumentSync = .optionA(.init(openClose: true, change: .full)) + capabilities.semanticTokensProvider = .optionA(.init(legend: .init(tokenTypes: [], tokenModifiers: []))) + server = LanguageServerType( + languageId: .swift, + binary: .init(execPath: "", args: [], env: nil), + lspInstance: InitializingServer( + server: BufferingServerConnection(), + initializeParamsProvider: LanguageServerType.getInitParams(workspacePath: "/") + ), + serverCapabilities: capabilities, + rootPath: URL(fileURLWithPath: "") + ) + _ = try await server.lspInstance.initializeIfNeeded() + document = MockDocumentType() + } + + // MARK: - Tests + + func testOpenDocumentRegistersObjects() async throws { + try await server.openDocument(document) + XCTAssertNotNil(document.languageServerObjects.highlightProvider) + XCTAssertNotNil(document.languageServerObjects.textCoordinator) + XCTAssertNotNil(server.openFiles.document(for: document.languageServerURI ?? "")) + } + + func testCloseDocumentClearsObjects() async throws { + guard let languageServerURI = document.languageServerURI else { + XCTFail("Language server URI missing on a mock object") + return + } + try await server.openDocument(document) + XCTAssertNotNil(server.openFiles.document(for: languageServerURI)) + + try await server.closeDocument(languageServerURI) + XCTAssertNil(document.languageServerObjects.highlightProvider) + XCTAssertNil(document.languageServerObjects.textCoordinator) + } +} diff --git a/CodeEditTests/Features/LSP/SemanticTokenMapTests.swift b/CodeEditTests/Features/LSP/SemanticTokens/SemanticTokenMapTests.swift similarity index 100% rename from CodeEditTests/Features/LSP/SemanticTokenMapTests.swift rename to CodeEditTests/Features/LSP/SemanticTokens/SemanticTokenMapTests.swift diff --git a/CodeEditTests/Features/LSP/SemanticTokenStorageTests.swift b/CodeEditTests/Features/LSP/SemanticTokens/SemanticTokenStorageTests.swift similarity index 75% rename from CodeEditTests/Features/LSP/SemanticTokenStorageTests.swift rename to CodeEditTests/Features/LSP/SemanticTokens/SemanticTokenStorageTests.swift index 22d9ecff3..8e7539140 100644 --- a/CodeEditTests/Features/LSP/SemanticTokenStorageTests.swift +++ b/CodeEditTests/Features/LSP/SemanticTokens/SemanticTokenStorageTests.swift @@ -12,9 +12,9 @@ import LanguageServerProtocol final class SemanticTokenStorageTests: XCTestCase { func testInvalidation() { - let storage = LSPSemanticTokenStorage() - storage.state = LSPSemanticTokenStorage.CurrentState( - requestId: nil, + let storage = SemanticTokenStorage() + storage.state = SemanticTokenStorage.CurrentState( + resultId: nil, tokenData: [0, 0, 2, 0, 0], tokens: [SemanticToken(line: 0, char: 0, length: 2, type: 0, modifiers: 0)] ) From 5edf93ffa41a86f6bfaa81d3e74875bdf1416f4d Mon Sep 17 00:00:00 2001 From: Khan Winter <35942988+thecoolwinter@users.noreply.github.com> Date: Fri, 14 Feb 2025 09:56:50 -0600 Subject: [PATCH 09/18] Fix Bugs --- CodeEdit.xcodeproj/project.pbxproj | 18 ++++++++++-- .../xcshareddata/swiftpm/Package.resolved | 11 +------ .../CodeFileDocument/CodeFileDocument.swift | 9 +++++- .../DocumentSync/LSPContentCoordinator.swift | 10 +++---- .../SemanticTokenHighlightProvider.swift | 4 ++- .../SemanticTokens/SemanticTokenMap.swift | 2 -- .../SemanticTokenStorage.swift | 1 - .../LanguageServer+SemanticTokens.swift | 29 ++++++++++--------- .../LSP/LanguageServer/LanguageServer.swift | 2 +- .../LanguageServerFileMap.swift | 3 +- .../Features/LSP/Service/LSPService.swift | 12 ++++---- 11 files changed, 57 insertions(+), 44 deletions(-) diff --git a/CodeEdit.xcodeproj/project.pbxproj b/CodeEdit.xcodeproj/project.pbxproj index 2d4914532..a8a9f292a 100644 --- a/CodeEdit.xcodeproj/project.pbxproj +++ b/CodeEdit.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 55; + objectVersion = 60; objects = { /* Begin PBXBuildFile section */ @@ -376,6 +376,7 @@ 6C1A7E942D5D557D001B951C /* CodeEditSourceEditor in Frameworks */ = {isa = PBXBuildFile; productRef = 6C5246732D1E612700F57F11 /* CodeEditSourceEditor */; }; 6C1A7E962D5D5762001B951C /* LanguageServerDocument.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C1A7E952D5D5762001B951C /* LanguageServerDocument.swift */; }; 6C1A7E9B2D5D5E0E001B951C /* LanguageServer+DocumentObjects.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C1A7E9A2D5D5E0E001B951C /* LanguageServer+DocumentObjects.swift */; }; + 6C1A7EA02D5F9BFB001B951C /* CodeEditSourceEditor in Frameworks */ = {isa = PBXBuildFile; productRef = 6C1A7E9F2D5F9BFB001B951C /* CodeEditSourceEditor */; }; 6C1CC9982B1E770B0002349B /* AsyncFileIterator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C1CC9972B1E770B0002349B /* AsyncFileIterator.swift */; }; 6C1CC99B2B1E7CBC0002349B /* FindNavigatorIndexBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C1CC99A2B1E7CBC0002349B /* FindNavigatorIndexBar.swift */; }; 6C1F3DA22C18C55800F6DEF6 /* ShellIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C1F3DA12C18C55800F6DEF6 /* ShellIntegrationTests.swift */; }; @@ -1372,6 +1373,7 @@ 6C85BB402C2105ED00EB5DEF /* CodeEditKit in Frameworks */, 6C66C31329D05CDC00DE9ED2 /* GRDB in Frameworks */, 58F2EB1E292FB954004A9BDE /* Sparkle in Frameworks */, + 6C1A7EA02D5F9BFB001B951C /* CodeEditSourceEditor in Frameworks */, 6C147C4529A329350089B630 /* OrderedCollections in Frameworks */, 6CE21E872C650D2C0031B056 /* SwiftTerm in Frameworks */, 6CC00A8B2CBEF150004E8134 /* CodeEditSourceEditor in Frameworks */, @@ -3892,6 +3894,7 @@ 6CB94D022CA1205100E8651C /* AsyncAlgorithms */, 6CC00A8A2CBEF150004E8134 /* CodeEditSourceEditor */, 6C5246732D1E612700F57F11 /* CodeEditSourceEditor */, + 6C1A7E9F2D5F9BFB001B951C /* CodeEditSourceEditor */, ); productName = CodeEdit; productReference = B658FB2C27DA9E0F00EA4DBD /* CodeEdit.app */; @@ -3989,7 +3992,7 @@ 303E88462C276FD600EEA8D9 /* XCRemoteSwiftPackageReference "LanguageServerProtocol" */, 6C4E37FA2C73E00700AEE7B5 /* XCRemoteSwiftPackageReference "SwiftTerm" */, 6CB94D012CA1205100E8651C /* XCRemoteSwiftPackageReference "swift-async-algorithms" */, - 6C9DB9E22D55656300ACD86E /* XCRemoteSwiftPackageReference "CodeEditSourceEditor" */, + 6C1A7E9E2D5F9BFB001B951C /* XCLocalSwiftPackageReference "../CodeEditSourceEditor" */, ); productRefGroup = B658FB2D27DA9E0F00EA4DBD /* Products */; projectDirPath = ""; @@ -5795,6 +5798,13 @@ }; /* End XCConfigurationList section */ +/* Begin XCLocalSwiftPackageReference section */ + 6C1A7E9E2D5F9BFB001B951C /* XCLocalSwiftPackageReference "../CodeEditSourceEditor" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = ../CodeEditSourceEditor; + }; +/* End XCLocalSwiftPackageReference section */ + /* Begin XCRemoteSwiftPackageReference section */ 2816F592280CF50500DD548B /* XCRemoteSwiftPackageReference "CodeEditSymbols" */ = { isa = XCRemoteSwiftPackageReference; @@ -5974,6 +5984,10 @@ package = 6C147C4329A329350089B630 /* XCRemoteSwiftPackageReference "swift-collections" */; productName = OrderedCollections; }; + 6C1A7E9F2D5F9BFB001B951C /* CodeEditSourceEditor */ = { + isa = XCSwiftPackageProductDependency; + productName = CodeEditSourceEditor; + }; 6C4E37FB2C73E00700AEE7B5 /* SwiftTerm */ = { isa = XCSwiftPackageProductDependency; package = 6C4E37FA2C73E00700AEE7B5 /* XCRemoteSwiftPackageReference "SwiftTerm" */; diff --git a/CodeEdit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/CodeEdit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 36e0897c9..bc8460a75 100644 --- a/CodeEdit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/CodeEdit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "ac57a6899925c3e4ac6d43aed791c845c6fc24a4441b6a10297a207d951b7836", + "originHash" : "454498edc6f3f47f3616318caf54005bbbfd026d4f4355edda503b072bfe9814", "pins" : [ { "identity" : "anycodable", @@ -28,15 +28,6 @@ "version" : "0.1.20" } }, - { - "identity" : "codeeditsourceeditor", - "kind" : "remoteSourceControl", - "location" : "https://github.com/CodeEditApp/CodeEditSourceEditor", - "state" : { - "revision" : "6b2c945501f0a5c15d8aa6d159fb2550c391bdd0", - "version" : "0.10.0" - } - }, { "identity" : "codeeditsymbols", "kind" : "remoteSourceControl", diff --git a/CodeEdit/Features/Documents/CodeFileDocument/CodeFileDocument.swift b/CodeEdit/Features/Documents/CodeFileDocument/CodeFileDocument.swift index 9fd315875..909235d5c 100644 --- a/CodeEdit/Features/Documents/CodeFileDocument/CodeFileDocument.swift +++ b/CodeEdit/Features/Documents/CodeFileDocument/CodeFileDocument.swift @@ -212,5 +212,12 @@ final class CodeFileDocument: NSDocument, ObservableObject { extension CodeFileDocument: LanguageServerDocument { /// A stable string to use when identifying documents with language servers. - var languageServerURI: String? { fileURL?.absolutePath } + /// Needs to be a valid URI, so always returns with the `file://` prefix to indicate it's a file URI. + var languageServerURI: String? { + if let path = fileURL?.absolutePath { + return "file://" + path + } else { + return nil + } + } } diff --git a/CodeEdit/Features/LSP/Features/DocumentSync/LSPContentCoordinator.swift b/CodeEdit/Features/LSP/Features/DocumentSync/LSPContentCoordinator.swift index 28fbb4a5b..aed2a7ada 100644 --- a/CodeEdit/Features/LSP/Features/DocumentSync/LSPContentCoordinator.swift +++ b/CodeEdit/Features/LSP/Features/DocumentSync/LSPContentCoordinator.swift @@ -28,7 +28,6 @@ class LSPContentCoordinator: TextViewCoord } private var editedRange: LSPRange? - private var stream: AsyncStream? private var sequenceContinuation: AsyncStream.Continuation? private var task: Task? @@ -39,16 +38,17 @@ class LSPContentCoordinator: TextViewCoord init(documentURI: String, languageServer: LanguageServer) { self.documentURI = documentURI self.languageServer = languageServer - self.stream = AsyncStream { continuation in - self.sequenceContinuation = continuation - } setUpUpdatesTask() } func setUpUpdatesTask() { task?.cancel() - guard let stream else { return } + // Create this stream here so it's always set up when the text view is set up, rather than only once on init. + let stream = AsyncStream { continuation in + self.sequenceContinuation = continuation + } + task = Task.detached { [weak self] in // Send edit events every 250ms for await events in stream.chunked(by: .repeating(every: .milliseconds(250), clock: .continuous)) { diff --git a/CodeEdit/Features/LSP/Features/SemanticTokens/SemanticTokenHighlightProvider.swift b/CodeEdit/Features/LSP/Features/SemanticTokens/SemanticTokenHighlightProvider.swift index b685133e6..357d7a816 100644 --- a/CodeEdit/Features/LSP/Features/SemanticTokens/SemanticTokenHighlightProvider.swift +++ b/CodeEdit/Features/LSP/Features/SemanticTokens/SemanticTokenHighlightProvider.swift @@ -164,7 +164,9 @@ final class SemanticTokenHighlightProvider< return } let rawTokens = storage.getTokensFor(range: lspRange) - let highlights = tokenMap.decode(tokens: rawTokens, using: textView) + let highlights = tokenMap + .decode(tokens: rawTokens, using: textView) + .filter({ $0.capture != nil || !$0.modifiers.isEmpty }) completion(.success(highlights)) } } diff --git a/CodeEdit/Features/LSP/Features/SemanticTokens/SemanticTokenMap.swift b/CodeEdit/Features/LSP/Features/SemanticTokens/SemanticTokenMap.swift index 0e580d22d..317068a2d 100644 --- a/CodeEdit/Features/LSP/Features/SemanticTokens/SemanticTokenMap.swift +++ b/CodeEdit/Features/LSP/Features/SemanticTokens/SemanticTokenMap.swift @@ -72,8 +72,6 @@ struct SemanticTokenMap: Sendable { // swiftlint:enable line_length let type = Int(token.type) let capture = tokenTypeMap.indices.contains(type) ? tokenTypeMap[type] : nil -// print(token.line, token.char, token.length, range, capture, modifiers.values) - return HighlightRange( range: range, capture: capture, diff --git a/CodeEdit/Features/LSP/Features/SemanticTokens/SemanticTokenStorage/SemanticTokenStorage.swift b/CodeEdit/Features/LSP/Features/SemanticTokens/SemanticTokenStorage/SemanticTokenStorage.swift index 01ae7dfcc..4c9cc9006 100644 --- a/CodeEdit/Features/LSP/Features/SemanticTokens/SemanticTokenStorage/SemanticTokenStorage.swift +++ b/CodeEdit/Features/LSP/Features/SemanticTokens/SemanticTokenStorage/SemanticTokenStorage.swift @@ -59,7 +59,6 @@ final class SemanticTokenStorage: GenericSemanticTokenStorage { } func setData(_ data: borrowing SemanticTokens) { - print(data.decode()) state = CurrentState(resultId: data.resultId, tokenData: data.data, tokens: data.decode()) } diff --git a/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+SemanticTokens.swift b/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+SemanticTokens.swift index e6242a0b8..62aff2050 100644 --- a/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+SemanticTokens.swift +++ b/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+SemanticTokens.swift @@ -11,7 +11,7 @@ import LanguageServerProtocol extension LanguageServer { func requestSemanticTokens(for documentURI: String) async throws -> SemanticTokensResponse { do { - logger.log("Requesting all tokens") + // No logging, called too often let params = SemanticTokensParams(textDocument: TextDocumentIdentifier(uri: documentURI)) return try await lspInstance.semanticTokensFull(params) } catch { @@ -20,25 +20,26 @@ extension LanguageServer { } } - 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 { - logger.warning("requestSemanticTokens range: Error \(error)") - throw error - } - } + // Unused +// 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 { +// logger.warning("requestSemanticTokens range: Error \(error)") +// throw error +// } +// } func requestSemanticTokens( for documentURI: String, previousResultId: String ) async throws -> SemanticTokensDeltaResponse { do { - logger.log("Requesting delta tokens") + // No logging, called too often let params = SemanticTokensDeltaParams( textDocument: TextDocumentIdentifier(uri: documentURI), previousResultId: previousResultId diff --git a/CodeEdit/Features/LSP/LanguageServer/LanguageServer.swift b/CodeEdit/Features/LSP/LanguageServer/LanguageServer.swift index ef37c8896..395678499 100644 --- a/CodeEdit/Features/LSP/LanguageServer/LanguageServer.swift +++ b/CodeEdit/Features/LSP/LanguageServer/LanguageServer.swift @@ -220,7 +220,7 @@ class LanguageServer { processId: nil, locale: nil, rootPath: nil, - rootUri: workspacePath, + rootUri: "file://" + workspacePath, // Make it a URI initializationOptions: [], capabilities: capabilities, trace: nil, diff --git a/CodeEdit/Features/LSP/LanguageServer/LanguageServerFileMap.swift b/CodeEdit/Features/LSP/LanguageServer/LanguageServerFileMap.swift index 642d21703..fd71a06b7 100644 --- a/CodeEdit/Features/LSP/LanguageServer/LanguageServerFileMap.swift +++ b/CodeEdit/Features/LSP/LanguageServer/LanguageServerFileMap.swift @@ -54,8 +54,7 @@ class LanguageServerFileMap { } func document(for uri: DocumentUri) -> DocumentType? { - let url = URL(filePath: uri) - return trackedDocuments.object(forKey: url.absolutePath as NSString) + return trackedDocuments.object(forKey: uri as NSString) } func removeDocument(for document: DocumentType) { diff --git a/CodeEdit/Features/LSP/Service/LSPService.swift b/CodeEdit/Features/LSP/Service/LSPService.swift index 99366f3b3..bb2d91681 100644 --- a/CodeEdit/Features/LSP/Service/LSPService.swift +++ b/CodeEdit/Features/LSP/Service/LSPService.swift @@ -99,6 +99,8 @@ import CodeEditLanguages /// ``` @MainActor final class LSPService: ObservableObject { + typealias LanguageServerType = LanguageServer + let logger: Logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "", category: "LSPService") struct ClientKey: Hashable, Equatable { @@ -112,7 +114,7 @@ final class LSPService: ObservableObject { } /// Holds the active language clients - var languageClients: [ClientKey: LanguageServer] = [:] + var languageClients: [ClientKey: LanguageServerType] = [:] /// 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 @@ -162,7 +164,7 @@ final class LSPService: ObservableObject { } /// Gets the language client for the specified language - func languageClient(for languageId: LanguageIdentifier, workspacePath: String) -> LanguageServer? { + func languageClient(for languageId: LanguageIdentifier, workspacePath: String) -> LanguageServerType? { return languageClients[ClientKey(languageId, workspacePath)] } @@ -174,14 +176,14 @@ final class LSPService: ObservableObject { func startServer( for languageId: LanguageIdentifier, workspacePath: String - ) async throws -> LanguageServer { + ) async throws -> LanguageServerType { 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( + let server = try await LanguageServerType.createServer( for: languageId, with: serverBinary, workspacePath: workspacePath @@ -203,7 +205,7 @@ final class LSPService: ObservableObject { return } Task { - let languageServer: LanguageServer + let languageServer: LanguageServerType do { if let server = self.languageClients[ClientKey(lspLanguage, workspacePath)] { languageServer = server From e015c4b0fc2b43bac87ead1ecff29864abebd838 Mon Sep 17 00:00:00 2001 From: Khan Winter <35942988+thecoolwinter@users.noreply.github.com> Date: Fri, 14 Feb 2025 09:58:49 -0600 Subject: [PATCH 10/18] Docs --- CodeEdit/Features/Editor/Views/CodeFileView.swift | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/CodeEdit/Features/Editor/Views/CodeFileView.swift b/CodeEdit/Features/Editor/Views/CodeFileView.swift index d70a8c733..7b65b88cd 100644 --- a/CodeEdit/Features/Editor/Views/CodeFileView.swift +++ b/CodeEdit/Features/Editor/Views/CodeFileView.swift @@ -63,7 +63,7 @@ struct CodeFileView: View { self.textViewCoordinators = textViewCoordinators + [codeFile.contentCoordinator] - + [codeFile.languageServerObjects.textCoordinator].compactMap({ $0 }) + + [codeFile.languageServerObjects.textCoordinator].compactMap({ $0 }) self.isEditable = isEditable if let openOptions = codeFile.openOptions { @@ -163,7 +163,7 @@ struct CodeFileView: View { bracketPairHighlight = getBracketPairHighlight() } .onReceive(codeFile.$languageServerObjects) { languageServerObjects in - updateHighlightProviders(languageServerObjects.highlightProvider) + updateHighlightProviders(lspHighlightProvider: languageServerObjects.highlightProvider) } } @@ -185,8 +185,10 @@ struct CodeFileView: View { return .underline(color: color) } } - - private func updateHighlightProviders(_ lspHighlightProvider: HighlightProviding? = nil) { + + /// Updates the highlight providers array. + /// - Parameter lspHighlightProvider: The language server provider, if available. + private func updateHighlightProviders(lspHighlightProvider: HighlightProviding? = nil) { highlightProviders = [lspHighlightProvider].compactMap({ $0 }) + [treeSitterClient] } } From 1c1ad6aaa275b07d317caca945fdef95630bf692 Mon Sep 17 00:00:00 2001 From: Khan Winter <35942988+thecoolwinter@users.noreply.github.com> Date: Fri, 14 Feb 2025 10:09:53 -0600 Subject: [PATCH 11/18] Docs --- .../Features/Editor/Views/CodeFileView.swift | 1 + .../GenericSemanticTokenStorage.swift | 6 ++-- .../SemanticTokenRange.swift | 1 + .../SemanticTokenStorage.swift | 34 ++++++++++++++----- 4 files changed, 31 insertions(+), 11 deletions(-) diff --git a/CodeEdit/Features/Editor/Views/CodeFileView.swift b/CodeEdit/Features/Editor/Views/CodeFileView.swift index 7b65b88cd..5d965d1ee 100644 --- a/CodeEdit/Features/Editor/Views/CodeFileView.swift +++ b/CodeEdit/Features/Editor/Views/CodeFileView.swift @@ -163,6 +163,7 @@ struct CodeFileView: View { bracketPairHighlight = getBracketPairHighlight() } .onReceive(codeFile.$languageServerObjects) { languageServerObjects in + // This will not be called in single-file views (for now) but is safe to listen to either way updateHighlightProviders(lspHighlightProvider: languageServerObjects.highlightProvider) } } diff --git a/CodeEdit/Features/LSP/Features/SemanticTokens/SemanticTokenStorage/GenericSemanticTokenStorage.swift b/CodeEdit/Features/LSP/Features/SemanticTokens/SemanticTokenStorage/GenericSemanticTokenStorage.swift index ba4536911..a1a6e36ca 100644 --- a/CodeEdit/Features/LSP/Features/SemanticTokens/SemanticTokenStorage/GenericSemanticTokenStorage.swift +++ b/CodeEdit/Features/LSP/Features/SemanticTokens/SemanticTokenStorage/GenericSemanticTokenStorage.swift @@ -9,10 +9,10 @@ import Foundation import LanguageServerProtocol import CodeEditSourceEditor -/// Defines a protocol for an object to provide a storage mechanism for semantic tokens. +/// Defines a protocol for an object to provide storage for semantic tokens. /// -/// There is only one concrete type that conforms to this in CE, but this protocol is useful in testing. -/// See ``SemanticTokenStorage`` for use. +/// There is only one concrete type that conforms to this in CE, but this protocol is useful in testing. +/// See ``SemanticTokenStorage``. protocol GenericSemanticTokenStorage: AnyObject { var lastResultId: String? { get } var hasTokens: Bool { get } diff --git a/CodeEdit/Features/LSP/Features/SemanticTokens/SemanticTokenStorage/SemanticTokenRange.swift b/CodeEdit/Features/LSP/Features/SemanticTokens/SemanticTokenStorage/SemanticTokenRange.swift index 23bc5f063..6a7bfff6d 100644 --- a/CodeEdit/Features/LSP/Features/SemanticTokens/SemanticTokenStorage/SemanticTokenRange.swift +++ b/CodeEdit/Features/LSP/Features/SemanticTokens/SemanticTokenStorage/SemanticTokenRange.swift @@ -5,6 +5,7 @@ // Created by Khan Winter on 12/26/24. // +/// Represents the range of a semantic token. struct SemanticTokenRange { let line: UInt32 let char: UInt32 diff --git a/CodeEdit/Features/LSP/Features/SemanticTokens/SemanticTokenStorage/SemanticTokenStorage.swift b/CodeEdit/Features/LSP/Features/SemanticTokens/SemanticTokenStorage/SemanticTokenStorage.swift index 4c9cc9006..c6fea8b9b 100644 --- a/CodeEdit/Features/LSP/Features/SemanticTokens/SemanticTokenStorage/SemanticTokenStorage.swift +++ b/CodeEdit/Features/LSP/Features/SemanticTokens/SemanticTokenStorage/SemanticTokenStorage.swift @@ -9,10 +9,10 @@ import Foundation import LanguageServerProtocol import CodeEditSourceEditor -/// This class provides an efficient storage mechanism for semantic token data. +/// This class provides storage for semantic token data. /// -/// The LSP spec requires that clients keep the original compressed data to apply delta edits to. The delta updates may -/// come as a delta to a single number in the compressed array. This class maintains a current state of compressed +/// The LSP spec requires that clients keep the original compressed data to apply delta edits. Delta updates may +/// appear as a delta to a single number in the compressed array. This class maintains the current state of compressed /// tokens and their decoded counterparts. It supports applying delta updates from the language server. /// /// See ``SemanticTokenHighlightProvider`` for it's connection to the editor view. @@ -34,18 +34,23 @@ final class SemanticTokenStorage: GenericSemanticTokenStorage { var state: CurrentState? + /// Create an empty storage object. init() { state = nil } // MARK: - Storage Conformance - + + /// Finds all tokens in the given range. + /// - Parameter range: The range to query. + /// - Returns: All tokens found in the range. func getTokensFor(range: LSPRange) -> [SemanticToken] { guard let state = state, !state.tokens.isEmpty else { return [] } var tokens: [SemanticToken] = [] + // Perform a binary search guard var idx = findLowerBound(in: range, data: state.tokens[...]) else { return [] } @@ -57,7 +62,9 @@ final class SemanticTokenStorage: GenericSemanticTokenStorage { return tokens } - + + /// Clear the current state and set a new one. + /// - Parameter data: The semantic tokens to set as the current state. func setData(_ data: borrowing SemanticTokens) { state = CurrentState(resultId: data.resultId, tokenData: data.data, tokens: data.decode()) } @@ -67,10 +74,11 @@ final class SemanticTokenStorage: GenericSemanticTokenStorage { /// To calculate invalidated ranges: /// - Grabs all semantic tokens that *will* be updated and invalidates their ranges /// - Loops over all inserted tokens and invalidates their ranges - /// This may result in duplicated ranges. It's up to the caller to de-duplicate if necessary. + /// This may result in duplicated ranges. It's up to the caller to de-duplicate if necessary. See + /// ``SemanticTokenStorage/invalidatedRanges(startIdx:length:data:)``. /// /// - Parameter deltas: The deltas to apply. - /// - Returns: All ranges invalidated by the applied deltas. + /// - Returns: Ranges invalidated by the applied deltas. func applyDelta(_ deltas: SemanticTokensDelta) -> [SemanticTokenRange] { assert(state != nil, "State should be set before applying any deltas.") guard var tokenData = state?.tokenData else { return [] } @@ -117,7 +125,17 @@ final class SemanticTokenStorage: GenericSemanticTokenStorage { } // MARK: - Invalidated Indices - + + /// Calculate what document ranges are invalidated due to changes in the compressed token data. + /// + /// This overestimates invalidated ranges by assuming all tokens touched by a change are invalid. All this does is + /// find what tokens are being updated by a delta and return them. + /// + /// - Parameters: + /// - startIdx: The start index of the compressed token data an edits start at. + /// - length: The length of any edits. + /// - data: A reference to the compressed token data. + /// - Returns: All token ranges included in the range of the edit. func invalidatedRanges(startIdx: UInt, length: UInt, data: ArraySlice) -> [SemanticTokenRange] { var ranges: [SemanticTokenRange] = [] var idx = startIdx - (startIdx % 5) From 2606ce618c6f392df5ab5af7cf17c4b63e637fce Mon Sep 17 00:00:00 2001 From: Khan Winter <35942988+thecoolwinter@users.noreply.github.com> Date: Fri, 14 Feb 2025 10:15:52 -0600 Subject: [PATCH 12/18] Lint & Documentation --- CodeEdit/Features/Editor/Views/CodeFileView.swift | 2 +- .../Capabilities/LanguageServer+SemanticTokens.swift | 5 ++++- CodeEdit/Features/LSP/LanguageServer/LanguageServer.swift | 3 ++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/CodeEdit/Features/Editor/Views/CodeFileView.swift b/CodeEdit/Features/Editor/Views/CodeFileView.swift index 5d965d1ee..a634fd5fd 100644 --- a/CodeEdit/Features/Editor/Views/CodeFileView.swift +++ b/CodeEdit/Features/Editor/Views/CodeFileView.swift @@ -186,7 +186,7 @@ struct CodeFileView: View { return .underline(color: color) } } - + /// Updates the highlight providers array. /// - Parameter lspHighlightProvider: The language server provider, if available. private func updateHighlightProviders(lspHighlightProvider: HighlightProviding? = nil) { diff --git a/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+SemanticTokens.swift b/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+SemanticTokens.swift index 62aff2050..27e078c58 100644 --- a/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+SemanticTokens.swift +++ b/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+SemanticTokens.swift @@ -26,7 +26,10 @@ extension LanguageServer { // forRange range: LSPRange // ) async throws -> SemanticTokensResponse { // do { -// let params = SemanticTokensRangeParams(textDocument: TextDocumentIdentifier(uri: documentURI), range: range) +// let params = SemanticTokensRangeParams( +// textDocument: TextDocumentIdentifier(uri: documentURI), +// range: range +// ) // return try await lspInstance.semanticTokensRange(params) // } catch { // logger.warning("requestSemanticTokens range: Error \(error)") diff --git a/CodeEdit/Features/LSP/LanguageServer/LanguageServer.swift b/CodeEdit/Features/LSP/LanguageServer/LanguageServer.swift index 395678499..a7c48bb25 100644 --- a/CodeEdit/Features/LSP/LanguageServer/LanguageServer.swift +++ b/CodeEdit/Features/LSP/LanguageServer/LanguageServer.swift @@ -11,8 +11,9 @@ import LanguageClient import LanguageServerProtocol import OSLog +/// A client for language servers. class LanguageServer { - static var logger: Logger { + static var logger: Logger { // types with associated types cannot have constant static properties Logger(subsystem: Bundle.main.bundleIdentifier ?? "", category: "LanguageServer") } let logger: Logger From 12f92c0abb23417f88749699a7f61a460ac5a12b Mon Sep 17 00:00:00 2001 From: Khan Winter <35942988+thecoolwinter@users.noreply.github.com> Date: Sat, 8 Mar 2025 09:18:39 -0600 Subject: [PATCH 13/18] Update Package.swift, Resolve Linter --- .../xcshareddata/swiftpm/Package.resolved | 15 ++++++++++++--- .../SemanticTokenStorage.swift | 6 +++--- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/CodeEdit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/CodeEdit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index bc8460a75..958dc198d 100644 --- a/CodeEdit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/CodeEdit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "454498edc6f3f47f3616318caf54005bbbfd026d4f4355edda503b072bfe9814", + "originHash" : "ac57a6899925c3e4ac6d43aed791c845c6fc24a4441b6a10297a207d951b7836", "pins" : [ { "identity" : "anycodable", @@ -28,6 +28,15 @@ "version" : "0.1.20" } }, + { + "identity" : "codeeditsourceeditor", + "kind" : "remoteSourceControl", + "location" : "https://github.com/CodeEditApp/CodeEditSourceEditor", + "state" : { + "revision" : "6b2c945501f0a5c15d8aa6d159fb2550c391bdd0", + "version" : "0.10.0" + } + }, { "identity" : "codeeditsymbols", "kind" : "remoteSourceControl", @@ -213,8 +222,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/lukepistrol/SwiftLintPlugin", "state" : { - "revision" : "5a65f4074975f811da666dfe31a19850950b1ea4", - "version" : "0.56.2" + "revision" : "3825ebf8d55bb877c91bc897e8e3d0c001f16fba", + "version" : "0.58.2" } }, { diff --git a/CodeEdit/Features/LSP/Features/SemanticTokens/SemanticTokenStorage/SemanticTokenStorage.swift b/CodeEdit/Features/LSP/Features/SemanticTokens/SemanticTokenStorage/SemanticTokenStorage.swift index c6fea8b9b..9926ec25f 100644 --- a/CodeEdit/Features/LSP/Features/SemanticTokens/SemanticTokenStorage/SemanticTokenStorage.swift +++ b/CodeEdit/Features/LSP/Features/SemanticTokens/SemanticTokenStorage/SemanticTokenStorage.swift @@ -40,7 +40,7 @@ final class SemanticTokenStorage: GenericSemanticTokenStorage { } // MARK: - Storage Conformance - + /// Finds all tokens in the given range. /// - Parameter range: The range to query. /// - Returns: All tokens found in the range. @@ -62,7 +62,7 @@ final class SemanticTokenStorage: GenericSemanticTokenStorage { return tokens } - + /// Clear the current state and set a new one. /// - Parameter data: The semantic tokens to set as the current state. func setData(_ data: borrowing SemanticTokens) { @@ -125,7 +125,7 @@ final class SemanticTokenStorage: GenericSemanticTokenStorage { } // MARK: - Invalidated Indices - + /// Calculate what document ranges are invalidated due to changes in the compressed token data. /// /// This overestimates invalidated ranges by assuming all tokens touched by a change are invalid. All this does is From baafd2e3662529557380dd9de0a7cbc8a433651d Mon Sep 17 00:00:00 2001 From: Khan Winter <35942988+thecoolwinter@users.noreply.github.com> Date: Sat, 15 Mar 2025 08:58:16 -0500 Subject: [PATCH 14/18] Add Some Tests --- .../SemanticTokenMapTests.swift | 2 +- .../SemanticTokenStorageTests.swift | 80 +++++++++++++++++-- 2 files changed, 74 insertions(+), 8 deletions(-) diff --git a/CodeEditTests/Features/LSP/SemanticTokens/SemanticTokenMapTests.swift b/CodeEditTests/Features/LSP/SemanticTokens/SemanticTokenMapTests.swift index 4c941de1a..03c91b569 100644 --- a/CodeEditTests/Features/LSP/SemanticTokens/SemanticTokenMapTests.swift +++ b/CodeEditTests/Features/LSP/SemanticTokens/SemanticTokenMapTests.swift @@ -10,7 +10,7 @@ import CodeEditSourceEditor import LanguageServerProtocol @testable import CodeEdit -final class SemanticTokenMapTestsTests: XCTestCase { +final class SemanticTokenMapTests: 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? { diff --git a/CodeEditTests/Features/LSP/SemanticTokens/SemanticTokenStorageTests.swift b/CodeEditTests/Features/LSP/SemanticTokens/SemanticTokenStorageTests.swift index 8e7539140..a9ff60e69 100644 --- a/CodeEditTests/Features/LSP/SemanticTokens/SemanticTokenStorageTests.swift +++ b/CodeEditTests/Features/LSP/SemanticTokens/SemanticTokenStorageTests.swift @@ -5,18 +5,84 @@ // Created by Khan Winter on 12/26/24. // -import XCTest +import Foundation +import Testing import CodeEditSourceEditor import LanguageServerProtocol @testable import CodeEdit -final class SemanticTokenStorageTests: XCTestCase { - func testInvalidation() { +// For easier comparison while setting semantic tokens +extension SemanticToken: @retroactive Equatable { + public static func == (lhs: SemanticToken, rhs: SemanticToken) -> Bool { + lhs.type == rhs.type + && lhs.modifiers == rhs.modifiers + && lhs.line == rhs.line + && lhs.char == rhs.char + && lhs.length == rhs.length + } +} + +@Suite +struct SemanticTokenStorageTests { + let storage = SemanticTokenStorage() + + let semanticTokens = [ + SemanticToken(line: 0, char: 0, length: 10, type: 0, modifiers: 0), + SemanticToken(line: 1, char: 2, length: 5, type: 2, modifiers: 3), + SemanticToken(line: 3, char: 8, length: 10, type: 1, modifiers: 0) + ] + + @Test + func initialState() async throws { + #expect(storage.state == nil) + #expect(storage.hasTokens == false) + #expect(storage.lastResultId == nil) + } + + @Test + func setData() async throws { + storage.setData( + SemanticTokens( + resultId: "1234", + tokens: semanticTokens + ) + ) + + let state = try #require(storage.state) + #expect(state.tokens == semanticTokens) + #expect(state.resultId == "1234") + + #expect(storage.lastResultId == "1234") + #expect(storage.hasTokens == true) + } + + @Suite("ApplyDeltas") + struct TokensDeltasTests { let storage = SemanticTokenStorage() - storage.state = SemanticTokenStorage.CurrentState( - resultId: nil, - tokenData: [0, 0, 2, 0, 0], - tokens: [SemanticToken(line: 0, char: 0, length: 2, type: 0, modifiers: 0)] + + let semanticTokens = [ + SemanticToken(line: 0, char: 0, length: 10, type: 0, modifiers: 0), + SemanticToken(line: 1, char: 2, length: 5, type: 2, modifiers: 3), + SemanticToken(line: 3, char: 8, length: 10, type: 1, modifiers: 0) + ] + + @Test( + arguments: [ + #"{ "resultId": "1", "edits": [{"start": 0, "deleteCount": 0, "data": [0, 2, 3, 0, 1] }] }"# + ] ) + func applyDeltas(deltasJSON: String) async throws { + // This is unfortunate, but there's no public initializer for these structs. + // So we have to decode them from JSON strings + let decoder = JSONDecoder() + let deltas = try decoder.decode(SemanticTokensDelta.self, from: Data(deltasJSON.utf8)) + + + } + + @Test + func invalidatedRanges() { + + } } } From aba0a0b02953753e4643e5926d13e1976f85a68c Mon Sep 17 00:00:00 2001 From: Khan Winter <35942988+thecoolwinter@users.noreply.github.com> Date: Sat, 15 Mar 2025 09:35:27 -0500 Subject: [PATCH 15/18] Finish Deltas Tests, Rename `hasTokens` --- .../SemanticTokenHighlightProvider.swift | 4 +- .../GenericSemanticTokenStorage.swift | 2 +- .../SemanticTokenStorage.swift | 20 +-- .../SemanticTokenStorageTests.swift | 141 ++++++++++++++++-- 4 files changed, 136 insertions(+), 31 deletions(-) diff --git a/CodeEdit/Features/LSP/Features/SemanticTokens/SemanticTokenHighlightProvider.swift b/CodeEdit/Features/LSP/Features/SemanticTokens/SemanticTokenHighlightProvider.swift index 357d7a816..2e391fba4 100644 --- a/CodeEdit/Features/LSP/Features/SemanticTokens/SemanticTokenHighlightProvider.swift +++ b/CodeEdit/Features/LSP/Features/SemanticTokens/SemanticTokenHighlightProvider.swift @@ -66,7 +66,7 @@ final class SemanticTokenHighlightProvider< return } - guard storage.hasTokens else { + guard storage.hasReceivedData else { // We have no semantic token info, request it! try await requestTokens(languageServer: languageServer, textView: textView) await MainActor.run { @@ -154,7 +154,7 @@ final class SemanticTokenHighlightProvider< } func queryHighlightsFor(textView: TextView, range: NSRange, completion: @escaping HighlightCallback) { - guard storage.hasTokens else { + guard storage.hasReceivedData else { pendingHighlightCallbacks.append(completion) return } diff --git a/CodeEdit/Features/LSP/Features/SemanticTokens/SemanticTokenStorage/GenericSemanticTokenStorage.swift b/CodeEdit/Features/LSP/Features/SemanticTokens/SemanticTokenStorage/GenericSemanticTokenStorage.swift index a1a6e36ca..ecfcb3932 100644 --- a/CodeEdit/Features/LSP/Features/SemanticTokens/SemanticTokenStorage/GenericSemanticTokenStorage.swift +++ b/CodeEdit/Features/LSP/Features/SemanticTokens/SemanticTokenStorage/GenericSemanticTokenStorage.swift @@ -15,7 +15,7 @@ import CodeEditSourceEditor /// See ``SemanticTokenStorage``. protocol GenericSemanticTokenStorage: AnyObject { var lastResultId: String? { get } - var hasTokens: Bool { get } + var hasReceivedData: Bool { get } init() diff --git a/CodeEdit/Features/LSP/Features/SemanticTokens/SemanticTokenStorage/SemanticTokenStorage.swift b/CodeEdit/Features/LSP/Features/SemanticTokens/SemanticTokenStorage/SemanticTokenStorage.swift index 9926ec25f..3faeae250 100644 --- a/CodeEdit/Features/LSP/Features/SemanticTokens/SemanticTokenStorage/SemanticTokenStorage.swift +++ b/CodeEdit/Features/LSP/Features/SemanticTokens/SemanticTokenStorage/SemanticTokenStorage.swift @@ -24,11 +24,15 @@ final class SemanticTokenStorage: GenericSemanticTokenStorage { let tokens: [SemanticToken] } + /// The last received result identifier. var lastResultId: String? { state?.resultId } - var hasTokens: Bool { + /// Indicates if the storage object has received any data. + /// Once `setData` has been called, this returns `true`. + /// Other operations will fail without any data in the storage object. + var hasReceivedData: Bool { state != nil } @@ -108,19 +112,9 @@ final class SemanticTokenStorage: GenericSemanticTokenStorage { } } - // Set the current state and decode the new token data - var decodedTokens: [SemanticToken] = [] - for idx in stride(from: 0, to: tokenData.count, by: 5) { - decodedTokens.append(SemanticToken( - line: tokenData[idx], - char: tokenData[idx + 1], - length: tokenData[idx + 2], - type: tokenData[idx + 3], - modifiers: tokenData[idx + 4] - )) - } + // Re-decode the updated token data and set the updated state + let decodedTokens = SemanticTokens(data: tokenData).decode() state = CurrentState(resultId: deltas.resultId, tokenData: tokenData, tokens: decodedTokens) - return invalidatedSet } diff --git a/CodeEditTests/Features/LSP/SemanticTokens/SemanticTokenStorageTests.swift b/CodeEditTests/Features/LSP/SemanticTokens/SemanticTokenStorageTests.swift index a9ff60e69..f2d0179ca 100644 --- a/CodeEditTests/Features/LSP/SemanticTokens/SemanticTokenStorageTests.swift +++ b/CodeEditTests/Features/LSP/SemanticTokens/SemanticTokenStorageTests.swift @@ -35,7 +35,7 @@ struct SemanticTokenStorageTests { @Test func initialState() async throws { #expect(storage.state == nil) - #expect(storage.hasTokens == false) + #expect(storage.hasReceivedData == false) #expect(storage.lastResultId == nil) } @@ -53,12 +53,51 @@ struct SemanticTokenStorageTests { #expect(state.resultId == "1234") #expect(storage.lastResultId == "1234") - #expect(storage.hasTokens == true) + #expect(storage.hasReceivedData == true) + } + + @Test + func overwriteDataRepeatedly() async throws { + let dataToApply: [(String?, [SemanticToken])] = [ + (nil, semanticTokens), + ("1", []), + ("2", semanticTokens.dropLast()), + ("3", semanticTokens) + ] + for (resultId, tokens) in dataToApply { + storage.setData(SemanticTokens(resultId: resultId, tokens: tokens)) + let state = try #require(storage.state) + #expect(state.tokens == tokens) + #expect(state.resultId == resultId) + #expect(storage.lastResultId == resultId) + #expect(storage.hasReceivedData == true) + } } @Suite("ApplyDeltas") struct TokensDeltasTests { - let storage = SemanticTokenStorage() + struct DeltaEdit { + let start: Int + let deleteCount: Int + let data: [Int] + + func makeString() -> String { + let dataString = data.map { String($0) }.joined(separator: ",") + return "{\"start\": \(start), \"deleteCount\": \(deleteCount), \"data\": [\(dataString)] }" + } + } + + func makeDelta(resultId: String, edits: [DeltaEdit]) throws -> SemanticTokensDelta { + // This is unfortunate, but there's no public initializer for these structs. + // So we have to decode them from JSON strings + let editsString = edits.map { $0.makeString() }.joined(separator: ",") + let deltasJSON = "{ \"resultId\": \"\(resultId)\", \"edits\": [\(editsString)] }" + let decoder = JSONDecoder() + let deltas = try decoder.decode(SemanticTokensDelta.self, from: Data(deltasJSON.utf8)) + return deltas + } + + let storage: SemanticTokenStorage let semanticTokens = [ SemanticToken(line: 0, char: 0, length: 10, type: 0, modifiers: 0), @@ -66,23 +105,95 @@ struct SemanticTokenStorageTests { SemanticToken(line: 3, char: 8, length: 10, type: 1, modifiers: 0) ] - @Test( - arguments: [ - #"{ "resultId": "1", "edits": [{"start": 0, "deleteCount": 0, "data": [0, 2, 3, 0, 1] }] }"# - ] - ) - func applyDeltas(deltasJSON: String) async throws { - // This is unfortunate, but there's no public initializer for these structs. - // So we have to decode them from JSON strings - let decoder = JSONDecoder() - let deltas = try decoder.decode(SemanticTokensDelta.self, from: Data(deltasJSON.utf8)) + init() { + storage = SemanticTokenStorage() + storage.setData(SemanticTokens(tokens: semanticTokens)) + #expect(storage.state?.tokens == semanticTokens) + } + + @Test + func applyEmptyDeltasNoChange() throws { + let deltas = try makeDelta(resultId: "1", edits: []) + + _ = storage.applyDelta(deltas) + + let state = try #require(storage.state) + #expect(state.tokens.count == 3) + #expect(state.resultId == "1") + #expect(state.tokens == semanticTokens) + } + + @Test + func applyInsertDeltas() throws { + let deltas = try makeDelta(resultId: "1", edits: [.init(start: 0, deleteCount: 0, data: [0, 2, 3, 0, 1])]) + + _ = storage.applyDelta(deltas) + + let state = try #require(storage.state) + #expect(state.tokens.count == 4) + #expect(storage.lastResultId == "1") + + // Should have inserted one at the beginning + #expect(state.tokens[0].line == 0) + #expect(state.tokens[0].char == 2) + #expect(state.tokens[0].length == 3) + #expect(state.tokens[0].modifiers == 1) + + // We inserted a delta into the space before this one (at char 2) so this one starts at the same spot + #expect(state.tokens[1] == SemanticToken(line: 0, char: 2, length: 10, type: 0, modifiers: 0)) + #expect(state.tokens[2] == semanticTokens[1]) + #expect(state.tokens[3] == semanticTokens[2]) + } + + @Test + func applyDeleteOneDeltas() throws { + // Delete the second token (semanticTokens[1]) from the initial state. + // Each token is represented by 5 numbers, so token[1] starts at raw data index 5. + let deltas = try makeDelta(resultId: "2", edits: [.init(start: 5, deleteCount: 5, data: [])]) + _ = storage.applyDelta(deltas) + + let state = try #require(storage.state) + #expect(state.tokens.count == 2) + #expect(state.resultId == "2") + // The remaining tokens should be the first and third tokens, except we deleted one line between them + // so the third token's line is less one + #expect(state.tokens[0] == semanticTokens[0]) + #expect(state.tokens[1] == SemanticToken(line: 2, char: 8, length: 10, type: 1, modifiers: 0)) + } + + @Test + func applyDeleteManyDeltas() throws { + // Delete the first two tokens from the initial state. + // Token[0] and token[1] together use 10 integers. + let deltas = try makeDelta(resultId: "3", edits: [.init(start: 0, deleteCount: 10, data: [])]) + _ = storage.applyDelta(deltas) - + let state = try #require(storage.state) + #expect(state.tokens.count == 1) + #expect(state.resultId == "3") + // The only remaining token should be the original third token. + #expect(state.tokens[0] == SemanticToken(line: 2, char: 8, length: 10, type: 1, modifiers: 0)) } @Test - func invalidatedRanges() { + func applyInsertAndDeleteDeltas() throws { + // Combined test: insert a token at the beginning and delete the last token. + // Edit 1: Insert a new token at the beginning. + let insertion = DeltaEdit(start: 0, deleteCount: 0, data: [0, 2, 3, 0, 1]) + // Edit 2: Delete the token that starts at raw data index 10 (the third token in the original state). + let deletion = DeltaEdit(start: 10, deleteCount: 5, data: []) + let deltas = try makeDelta(resultId: "4", edits: [insertion, deletion]) + _ = storage.applyDelta(deltas) + let state = try #require(storage.state) + #expect(state.tokens.count == 3) + #expect(storage.lastResultId == "4") + // The new inserted token becomes the first token. + #expect(state.tokens[0] == SemanticToken(line: 0, char: 2, length: 3, type: 0, modifiers: 1)) + // The original first token is shifted (its character offset increased by 2). + #expect(state.tokens[1] == SemanticToken(line: 0, char: 2, length: 10, type: 0, modifiers: 0)) + // The second token from the original state remains unchanged. + #expect(state.tokens[2] == semanticTokens[1]) } } } From 59447f4fe3e8ac1e4797f5419f9601d8e993c6a5 Mon Sep 17 00:00:00 2001 From: Khan Winter <35942988+thecoolwinter@users.noreply.github.com> Date: Sat, 15 Mar 2025 09:53:14 -0500 Subject: [PATCH 16/18] SemanticTokenMapTests Asserted Incorrect Case --- .../SemanticTokenMapTests.swift | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/CodeEditTests/Features/LSP/SemanticTokens/SemanticTokenMapTests.swift b/CodeEditTests/Features/LSP/SemanticTokens/SemanticTokenMapTests.swift index 03c91b569..a9ec5c5a3 100644 --- a/CodeEditTests/Features/LSP/SemanticTokens/SemanticTokenMapTests.swift +++ b/CodeEditTests/Features/LSP/SemanticTokens/SemanticTokenMapTests.swift @@ -53,10 +53,10 @@ final class SemanticTokenMapTests: XCTestCase { // 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: 0, length: 1, type: 1000000, 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: 4, length: 1, type: 1, modifiers: 0b101), + SemanticToken(line: 0, char: 5, length: 1, type: 4, modifiers: 0b1010), SemanticToken(line: 0, char: 7, length: 10, type: 0, modifiers: 0) ]) let decoded = map.decode(tokens: tokens, using: mockProvider) @@ -69,10 +69,10 @@ final class SemanticTokenMapTests: XCTestCase { 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[1].capture, .include, "No Decoded Capture") + XCTAssertEqual(decoded[2].capture, .constructor, "Decoded Capture") + XCTAssertEqual(decoded[3].capture, .comment, "Decoded Capture") + XCTAssertEqual(decoded[4].capture, .include, "No Decoded Capture") XCTAssertEqual(decoded[0].modifiers, [.declaration, .definition], "Decoded Modifiers") XCTAssertEqual(decoded[1].modifiers, [.readonly, .defaultLibrary], "Decoded Modifiers") @@ -92,10 +92,10 @@ final class SemanticTokenMapTests: XCTestCase { // 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: 0, length: 1, type: 100, 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: 4, length: 1, type: 1, modifiers: 0b101), + SemanticToken(line: 0, char: 5, length: 1, type: 4, modifiers: 0b1010), SemanticToken(line: 0, char: 7, length: 10, type: 0, modifiers: 0) ]) let decoded = map.decode(tokens: tokens, using: mockProvider) @@ -108,10 +108,10 @@ final class SemanticTokenMapTests: XCTestCase { 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[1].capture, .include, "No Decoded Capture") + XCTAssertEqual(decoded[2].capture, .constructor, "Decoded Capture") + XCTAssertEqual(decoded[3].capture, .comment, "Decoded Capture") + XCTAssertEqual(decoded[4].capture, .include, "No Decoded Capture") XCTAssertEqual(decoded[0].modifiers, [.declaration, .definition], "Decoded Modifiers") XCTAssertEqual(decoded[1].modifiers, [.readonly, .defaultLibrary], "Decoded Modifiers") From 04da78f9ee057f3c88125f6681b0568489c794ce Mon Sep 17 00:00:00 2001 From: Khan Winter <35942988+thecoolwinter@users.noreply.github.com> Date: Mon, 24 Mar 2025 15:59:38 -0500 Subject: [PATCH 17/18] Clean Tests Slightly, Add `lspURI` to `URL` type. --- .../CodeFileDocument/CodeFileDocument.swift | 6 +- .../Features/LSP/Service/LSPService.swift | 37 ++++--- .../LSP/Service/LSPServiceError.swift | 13 +++ .../Utils/Extensions/URL/URL+LSPURI.swift | 18 ++++ .../LSP/LanguageServer+CodeFileDocument.swift | 98 ++++++++++--------- 5 files changed, 103 insertions(+), 69 deletions(-) create mode 100644 CodeEdit/Features/LSP/Service/LSPServiceError.swift create mode 100644 CodeEdit/Utils/Extensions/URL/URL+LSPURI.swift diff --git a/CodeEdit/Features/Documents/CodeFileDocument/CodeFileDocument.swift b/CodeEdit/Features/Documents/CodeFileDocument/CodeFileDocument.swift index d8a2a89f0..2b3fcfd24 100644 --- a/CodeEdit/Features/Documents/CodeFileDocument/CodeFileDocument.swift +++ b/CodeEdit/Features/Documents/CodeFileDocument/CodeFileDocument.swift @@ -231,10 +231,6 @@ extension CodeFileDocument: LanguageServerDocument { /// A stable string to use when identifying documents with language servers. /// Needs to be a valid URI, so always returns with the `file://` prefix to indicate it's a file URI. var languageServerURI: String? { - if let path = fileURL?.absolutePath { - return "file://" + path - } else { - return nil - } + fileURL?.lspURI } } diff --git a/CodeEdit/Features/LSP/Service/LSPService.swift b/CodeEdit/Features/LSP/Service/LSPService.swift index bb2d91681..df74fb139 100644 --- a/CodeEdit/Features/LSP/Service/LSPService.swift +++ b/CodeEdit/Features/LSP/Service/LSPService.swift @@ -42,7 +42,7 @@ import CodeEditLanguages /// do { /// guard var languageClient = self.languageClient(for: .python) else { /// print("Failed to get client") -/// throw ServerManagerError.languageClientNotFound +/// throw LSPServiceError.languageClientNotFound /// } /// /// let testFilePathStr = "" @@ -54,7 +54,7 @@ import CodeEditLanguages /// // Completion example /// let textPosition = Position(line: 32, character: 18) // Lines and characters start at 0 /// let completions = try await languageClient.requestCompletion( -/// document: testFileURL.absoluteString, +/// document: testFileURL.lspURI, /// position: textPosition /// ) /// switch completions { @@ -168,6 +168,12 @@ final class LSPService: ObservableObject { return languageClients[ClientKey(languageId, workspacePath)] } + func languageClient(forDocument url: URL) -> LanguageServerType? { + languageClients.values.first(where: { $0.openFiles.document(for: url.lspURI) != nil }) + } + + // MARK: - Start Server + /// Given a language and workspace path, will attempt to start the language server /// - Parameters: /// - languageId: The ID of the language server to start. @@ -195,6 +201,8 @@ final class LSPService: ObservableObject { return server } + // MARK: - Document Management + /// 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. @@ -230,21 +238,19 @@ final class LSPService: ObservableObject { /// Notify all relevant language clients that a document was closed. /// - 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 - } + guard let languageClient = languageClient(forDocument: url) else { return } Task { do { - try await languageClient.closeDocument(url.absolutePath) + try await languageClient.closeDocument(url.lspURI) } catch { // swiftlint:disable:next line_length - logger.error("Failed to close document: \(url.absolutePath, privacy: .private), language: \(languageClient.languageId.rawValue). Error \(error)") + logger.error("Failed to close document: \(url.lspURI, privacy: .private), language: \(languageClient.languageId.rawValue). Error \(error)") } } } + // MARK: - Close Workspace + /// Close all language clients for a workspace. /// /// This is intentionally synchronous so we can exit from the workspace document's ``WorkspaceDocument/close()`` @@ -268,6 +274,8 @@ final class LSPService: ObservableObject { } } + // MARK: - Stop Servers + /// 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: @@ -276,7 +284,7 @@ final class LSPService: ObservableObject { func stopServer(forLanguage languageId: LanguageIdentifier, workspacePath: String) async throws { guard let server = server(for: languageId, workspacePath: workspacePath) else { logger.error("Server not found for language \(languageId.rawValue) during stop operation") - throw ServerManagerError.serverNotFound + throw LSPServiceError.serverNotFound } do { try await server.shutdownAndExit() @@ -311,12 +319,3 @@ final class LSPService: ObservableObject { eventListeningTasks.removeAll() } } - -// MARK: - Errors - -enum ServerManagerError: Error { - case serverNotFound - case serverStartFailed - case serverStopFailed - case languageClientNotFound -} diff --git a/CodeEdit/Features/LSP/Service/LSPServiceError.swift b/CodeEdit/Features/LSP/Service/LSPServiceError.swift new file mode 100644 index 000000000..d542e4d75 --- /dev/null +++ b/CodeEdit/Features/LSP/Service/LSPServiceError.swift @@ -0,0 +1,13 @@ +// +// LSPServiceError.swift +// CodeEdit +// +// Created by Khan Winter on 3/24/25. +// + +enum LSPServiceError: Error { + case serverNotFound + case serverStartFailed + case serverStopFailed + case languageClientNotFound +} diff --git a/CodeEdit/Utils/Extensions/URL/URL+LSPURI.swift b/CodeEdit/Utils/Extensions/URL/URL+LSPURI.swift new file mode 100644 index 000000000..f29f9057c --- /dev/null +++ b/CodeEdit/Utils/Extensions/URL/URL+LSPURI.swift @@ -0,0 +1,18 @@ +// +// URL+LSPURI.swift +// CodeEdit +// +// Created by Khan Winter on 3/24/25. +// + +import Foundation + +extension URL { + /// A stable string to use when identifying documents with language servers. + /// Needs to be a valid URI, so always returns with the `file://` prefix to indicate it's a file URI. + /// + /// Use this whenever possible when using USLs in LSP processing if not using the ``LanguageServerDocument`` type. + var lspURI: String { + return "file://" + absolutePath + } +} diff --git a/CodeEditTests/Features/LSP/LanguageServer+CodeFileDocument.swift b/CodeEditTests/Features/LSP/LanguageServer+CodeFileDocument.swift index 0cfa75a7d..236f2a721 100644 --- a/CodeEditTests/Features/LSP/LanguageServer+CodeFileDocument.swift +++ b/CodeEditTests/Features/LSP/LanguageServer+CodeFileDocument.swift @@ -13,6 +13,10 @@ import LanguageServerProtocol @testable import CodeEdit +/// This is an integration test for notifications relating to the ``CodeFileDocument`` class. +/// +/// For *unit* tests with the language server class, add tests to the `LanguageServer+DocumentObjects` test class as +/// it's cleaner and makes correct use of the mock document type. final class LanguageServerCodeFileDocumentTests: XCTestCase { // Test opening documents in CodeEdit triggers creating a language server, // further opened documents don't create new servers @@ -97,8 +101,11 @@ final class LanguageServerCodeFileDocumentTests: XCTestCase { // This is usually sent from the LSPService try await server.openDocument(codeFile) - await waitForClientEventCount( - 3, + await waitForClientState( + ( + [.initialize], + [.initialized, .textDocumentDidOpen] + ), connection: connection, description: "Initialized (2) and opened (1) notification count" ) @@ -110,15 +117,18 @@ final class LanguageServerCodeFileDocumentTests: XCTestCase { return codeFile } - func waitForClientEventCount(_ count: Int, connection: BufferingServerConnection, description: String) async { + func waitForClientState( + _ expectedValue: ([ClientRequest.Method], [ClientNotification.Method]), + connection: BufferingServerConnection, + description: String + ) async { let expectation = expectation(description: description) await withTaskGroup(of: Void.self) { group in + group.addTask { await self.fulfillment(of: [expectation], timeout: 2) } 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 { + for await events in connection.clientEventSequence + where events.0.map(\.method) == expectedValue.0 && events.1.map(\.method) == expectedValue.1 { expectation.fulfill() return } @@ -126,6 +136,8 @@ final class LanguageServerCodeFileDocumentTests: XCTestCase { } } + // MARK: - Open Close + @MainActor func testOpenCloseFileNotifications() async throws { // Set up test server @@ -155,30 +167,30 @@ final class LanguageServerCodeFileDocumentTests: XCTestCase { file.fileDocument = codeFile CodeEditDocumentController.shared.addDocument(codeFile) - await waitForClientEventCount(3, connection: connection, description: "Pre-close event count") + await waitForClientState( + ( + [.initialize], + [.initialized, .textDocumentDidOpen] + ), + connection: connection, + description: "Pre-close event count" + ) // This should then trigger a documentDidClose event codeFile.close() - await waitForClientEventCount(4, connection: connection, description: "Post-close event count") - - XCTAssertEqual( - connection.clientRequests.map { $0.method }, - [ - ClientRequest.Method.initialize, - ] - ) - - XCTAssertEqual( - connection.clientNotifications.map { $0.method }, - [ - ClientNotification.Method.initialized, - ClientNotification.Method.textDocumentDidOpen, - ClientNotification.Method.textDocumentDidClose - ] + await waitForClientState( + ( + [.initialize], + [.initialized, .textDocumentDidOpen, .textDocumentDidClose] + ), + connection: connection, + description: "Post-close event count" ) } + // MARK: - Test Document Edit + /// Assert the changed contents received by the buffered connection func assertExpectedContentChanges(connection: BufferingServerConnection, changes: [String]) { var foundChangeContents: [String] = [] @@ -186,9 +198,7 @@ final class LanguageServerCodeFileDocumentTests: XCTestCase { for notification in connection.clientNotifications { switch notification { case let .textDocumentDidChange(params): - foundChangeContents.append(contentsOf: params.contentChanges.map { event in - event.text - }) + foundChangeContents.append(contentsOf: params.contentChanges.map(\.text)) default: continue } @@ -233,18 +243,17 @@ final class LanguageServerCodeFileDocumentTests: XCTestCase { textView.replaceCharacters(in: NSRange(location: 39, length: 0), with: "World") // Added one notification - await waitForClientEventCount(4, connection: connection, description: "Edited notification count") + await waitForClientState( + ( + [.initialize], + [.initialized, .textDocumentDidOpen, .textDocumentDidChange] + ), + 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 } - ) // Expect only one change due to throttling. assertExpectedContentChanges( @@ -291,18 +300,17 @@ final class LanguageServerCodeFileDocumentTests: XCTestCase { textView.replaceCharacters(in: NSRange(location: 39, length: 0), with: "World") // Throttling means we should receive one edited notification + init notification + didOpen + init request - await waitForClientEventCount(4, connection: connection, description: "Edited notification count") + await waitForClientState( + ( + [.initialize], + [.initialized, .textDocumentDidOpen, .textDocumentDidChange] + ), + 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 } - ) // Expect three content changes. assertExpectedContentChanges( From b4aead17700f4b75d666d3a7d470401edc9c4594 Mon Sep 17 00:00:00 2001 From: Khan Winter <35942988+thecoolwinter@users.noreply.github.com> Date: Sat, 29 Mar 2025 13:48:13 -0500 Subject: [PATCH 18/18] Remove Unnecessary Comments --- .../LanguageServer+SemanticTokens.swift | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+SemanticTokens.swift b/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+SemanticTokens.swift index 27e078c58..b95098d02 100644 --- a/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+SemanticTokens.swift +++ b/CodeEdit/Features/LSP/LanguageServer/Capabilities/LanguageServer+SemanticTokens.swift @@ -11,7 +11,6 @@ import LanguageServerProtocol extension LanguageServer { func requestSemanticTokens(for documentURI: String) async throws -> SemanticTokensResponse { do { - // No logging, called too often let params = SemanticTokensParams(textDocument: TextDocumentIdentifier(uri: documentURI)) return try await lspInstance.semanticTokensFull(params) } catch { @@ -20,29 +19,11 @@ extension LanguageServer { } } - // Unused -// 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 { -// logger.warning("requestSemanticTokens range: Error \(error)") -// throw error -// } -// } - func requestSemanticTokens( for documentURI: String, previousResultId: String ) async throws -> SemanticTokensDeltaResponse { do { - // No logging, called too often let params = SemanticTokensDeltaParams( textDocument: TextDocumentIdentifier(uri: documentURI), previousResultId: previousResultId