1{-# LANGUAGE DuplicateRecordFields #-} 2{-# LANGUAGE TemplateHaskell #-} 3{-# LANGUAGE TypeOperators #-} 4{-# LANGUAGE OverloadedStrings #-} 5{-# LANGUAGE RecordWildCards #-} 6{-# LANGUAGE GeneralizedNewtypeDeriving #-} 7 8module Language.LSP.Types.WorkspaceEdit where 9 10import Control.Monad (unless) 11import Data.Aeson 12import Data.Aeson.TH 13import qualified Data.HashMap.Strict as H 14import Data.Maybe (catMaybes) 15import Data.Text (Text) 16import qualified Data.Text as T 17import Data.Hashable 18 19import Language.LSP.Types.Common 20import Language.LSP.Types.Location 21import Language.LSP.Types.TextDocument 22import Language.LSP.Types.Uri 23import Language.LSP.Types.Utils 24 25-- --------------------------------------------------------------------- 26 27data TextEdit = 28 TextEdit 29 { _range :: Range 30 , _newText :: Text 31 } deriving (Show,Read,Eq) 32 33deriveJSON lspOptions ''TextEdit 34 35-- --------------------------------------------------------------------- 36 37{-| 38Additional information that describes document changes. 39 40@since 3.16.0 41-} 42data ChangeAnnotation = 43 ChangeAnnotation 44 { -- | A human-readable string describing the actual change. The string 45 -- is rendered prominent in the user interface. 46 _label :: Text 47 -- | A flag which indicates that user confirmation is needed 48 -- before applying the change. 49 , _needsConfirmation :: Maybe Bool 50 -- | A human-readable string which is rendered less prominent in 51 -- the user interface. 52 , _description :: Maybe Text 53 } deriving (Show, Read, Eq) 54 55deriveJSON lspOptions ''ChangeAnnotation 56 57{-| 58An identifier referring to a change annotation managed by a workspace 59edit. 60 61@since 3.16.0 62-} 63newtype ChangeAnnotationIdentifier = ChangeAnnotationIdentifierId Text 64 deriving (Show, Read, Eq, FromJSON, ToJSON, ToJSONKey, FromJSONKey, Hashable) 65 66makeExtendingDatatype "AnnotatedTextEdit" [''TextEdit] 67 [("_annotationId", [t| ChangeAnnotationIdentifier |]) ] 68deriveJSON lspOptions ''AnnotatedTextEdit 69 70-- --------------------------------------------------------------------- 71 72data TextDocumentEdit = 73 TextDocumentEdit 74 { _textDocument :: VersionedTextDocumentIdentifier 75 , _edits :: List (TextEdit |? AnnotatedTextEdit) 76 } deriving (Show, Read, Eq) 77 78deriveJSON lspOptions ''TextDocumentEdit 79 80-- --------------------------------------------------------------------- 81 82-- | Options to create a file. 83data CreateFileOptions = 84 CreateFileOptions 85 { -- | Overwrite existing file. Overwrite wins over `ignoreIfExists` 86 _overwrite :: Maybe Bool 87 -- | Ignore if exists. 88 , _ignoreIfExists :: Maybe Bool 89 } deriving (Show, Read, Eq) 90 91deriveJSON lspOptions ''CreateFileOptions 92 93-- | Create file operation 94data CreateFile = 95 CreateFile 96 { -- | The resource to create. 97 _uri :: Uri 98 -- | Additional options 99 , _options :: Maybe CreateFileOptions 100 -- | An optional annotation identifer describing the operation. 101 -- 102 -- @since 3.16.0 103 , _annotationId :: Maybe ChangeAnnotationIdentifier 104 } deriving (Show, Read, Eq) 105 106instance ToJSON CreateFile where 107 toJSON CreateFile{..} = 108 object $ catMaybes 109 [ Just $ "kind" .= ("create" :: Text) 110 , Just $ "uri" .= _uri 111 , ("options" .=) <$> _options 112 , ("annotationId" .=) <$> _annotationId 113 ] 114 115instance FromJSON CreateFile where 116 parseJSON = withObject "CreateFile" $ \o -> do 117 kind <- o .: "kind" 118 unless (kind == ("create" :: Text)) 119 $ fail $ "Expected kind \"create\" but got " ++ show kind 120 _uri <- o .: "uri" 121 _options <- o .:? "options" 122 _annotationId <- o .:? "annotationId" 123 pure CreateFile{..} 124 125-- Rename file options 126data RenameFileOptions = 127 RenameFileOptions 128 { -- | Overwrite target if existing. Overwrite wins over `ignoreIfExists` 129 _overwrite :: Maybe Bool 130 -- | Ignores if target exists. 131 , _ignoreIfExists :: Maybe Bool 132 } deriving (Show, Read, Eq) 133 134deriveJSON lspOptions ''RenameFileOptions 135 136-- | Rename file operation 137data RenameFile = 138 RenameFile 139 { -- | The old (existing) location. 140 _oldUri :: Uri 141 -- | The new location. 142 , _newUri :: Uri 143 -- | Rename options. 144 , _options :: Maybe RenameFileOptions 145 -- | An optional annotation identifer describing the operation. 146 -- 147 -- @since 3.16.0 148 , _annotationId :: Maybe ChangeAnnotationIdentifier 149 } deriving (Show, Read, Eq) 150 151instance ToJSON RenameFile where 152 toJSON RenameFile{..} = 153 object $ catMaybes 154 [ Just $ "kind" .= ("rename" :: Text) 155 , Just $ "oldUri" .= _oldUri 156 , Just $ "newUri" .= _newUri 157 , ("options" .=) <$> _options 158 , ("annotationId" .=) <$> _annotationId 159 ] 160 161instance FromJSON RenameFile where 162 parseJSON = withObject "RenameFile" $ \o -> do 163 kind <- o .: "kind" 164 unless (kind == ("rename" :: Text)) 165 $ fail $ "Expected kind \"rename\" but got " ++ show kind 166 _oldUri <- o .: "oldUri" 167 _newUri <- o .: "newUri" 168 _options <- o .:? "options" 169 _annotationId <- o .:? "annotationId" 170 pure RenameFile{..} 171 172-- Delete file options 173data DeleteFileOptions = 174 DeleteFileOptions 175 { -- | Delete the content recursively if a folder is denoted. 176 _recursive :: Maybe Bool 177 -- | Ignore the operation if the file doesn't exist. 178 , _ignoreIfNotExists :: Maybe Bool 179 } deriving (Show, Read, Eq) 180 181deriveJSON lspOptions ''DeleteFileOptions 182 183-- | Delete file operation 184data DeleteFile = 185 DeleteFile 186 { -- | The file to delete. 187 _uri :: Uri 188 -- | Delete options. 189 , _options :: Maybe DeleteFileOptions 190 -- | An optional annotation identifer describing the operation. 191 -- 192 -- @since 3.16.0 193 , _annotationId :: Maybe ChangeAnnotationIdentifier 194 } deriving (Show, Read, Eq) 195 196instance ToJSON DeleteFile where 197 toJSON DeleteFile{..} = 198 object $ catMaybes 199 [ Just $ "kind" .= ("delete" :: Text) 200 , Just $ "uri" .= _uri 201 , ("options" .=) <$> _options 202 , ("annotationId" .=) <$> _annotationId 203 ] 204 205instance FromJSON DeleteFile where 206 parseJSON = withObject "DeleteFile" $ \o -> do 207 kind <- o .: "kind" 208 unless (kind == ("delete" :: Text)) 209 $ fail $ "Expected kind \"delete\" but got " ++ show kind 210 _uri <- o .: "uri" 211 _options <- o .:? "options" 212 _annotationId <- o .:? "annotationId" 213 pure DeleteFile{..} 214 215-- --------------------------------------------------------------------- 216 217-- | `TextDocumentEdit |? CreateFile |? RenameFile |? DeleteFile` is a bit mouthful, here's the synonym 218type DocumentChange = TextDocumentEdit |? CreateFile |? RenameFile |? DeleteFile 219 220-- --------------------------------------------------------------------- 221 222type WorkspaceEditMap = H.HashMap Uri (List TextEdit) 223type ChangeAnnotationMap = H.HashMap ChangeAnnotationIdentifier ChangeAnnotation 224 225data WorkspaceEdit = 226 WorkspaceEdit 227 { 228 -- | Holds changes to existing resources. 229 _changes :: Maybe WorkspaceEditMap 230 -- | Depending on the client capability 231 -- `workspace.workspaceEdit.resourceOperations` document changes are either 232 -- an array of `TextDocumentEdit`s to express changes to n different text 233 -- documents where each text document edit addresses a specific version of 234 -- a text document. Or it can contain above `TextDocumentEdit`s mixed with 235 -- create, rename and delete file / folder operations. 236 -- 237 -- Whether a client supports versioned document edits is expressed via 238 -- `workspace.workspaceEdit.documentChanges` client capability. 239 -- 240 -- If a client neither supports `documentChanges` nor 241 -- `workspace.workspaceEdit.resourceOperations` then only plain `TextEdit`s 242 -- using the `changes` property are supported. 243 , _documentChanges :: Maybe (List DocumentChange) 244 -- | A map of change annotations that can be referenced in 245 -- `AnnotatedTextEdit`s or create, rename and delete file / folder 246 -- operations. 247 -- 248 -- Whether clients honor this property depends on the client capability 249 -- `workspace.changeAnnotationSupport`. 250 -- 251 -- @since 3.16.0 252 , _changeAnnotations :: Maybe ChangeAnnotationMap 253 } deriving (Show, Read, Eq) 254 255instance Semigroup WorkspaceEdit where 256 (WorkspaceEdit a b c) <> (WorkspaceEdit a' b' c') = WorkspaceEdit (a <> a') (b <> b') (c <> c') 257instance Monoid WorkspaceEdit where 258 mempty = WorkspaceEdit Nothing Nothing Nothing 259 260deriveJSON lspOptions ''WorkspaceEdit 261 262-- ------------------------------------- 263 264data ResourceOperationKind 265 = ResourceOperationCreate -- ^ Supports creating new files and folders. 266 | ResourceOperationRename -- ^ Supports renaming existing files and folders. 267 | ResourceOperationDelete -- ^ Supports deleting existing files and folders. 268 deriving (Read, Show, Eq) 269 270instance ToJSON ResourceOperationKind where 271 toJSON ResourceOperationCreate = String "create" 272 toJSON ResourceOperationRename = String "rename" 273 toJSON ResourceOperationDelete = String "delete" 274 275instance FromJSON ResourceOperationKind where 276 parseJSON (String "create") = pure ResourceOperationCreate 277 parseJSON (String "rename") = pure ResourceOperationRename 278 parseJSON (String "delete") = pure ResourceOperationDelete 279 parseJSON _ = fail "ResourceOperationKind" 280 281data FailureHandlingKind 282 = FailureHandlingAbort -- ^ Applying the workspace change is simply aborted if one of the changes provided fails. All operations executed before the failing operation stay executed. 283 | FailureHandlingTransactional -- ^ All operations are executed transactional. That means they either all succeed or no changes at all are applied to the workspace. 284 | FailureHandlingTextOnlyTransactional -- ^ If the workspace edit contains only textual file changes they are executed transactional. If resource changes (create, rename or delete file) are part of the change the failure handling strategy is abort. 285 | FailureHandlingUndo -- ^ The client tries to undo the operations already executed. But there is no guarantee that this is succeeding. 286 deriving (Read, Show, Eq) 287 288instance ToJSON FailureHandlingKind where 289 toJSON FailureHandlingAbort = String "abort" 290 toJSON FailureHandlingTransactional = String "transactional" 291 toJSON FailureHandlingTextOnlyTransactional = String "textOnlyTransactional" 292 toJSON FailureHandlingUndo = String "undo" 293 294instance FromJSON FailureHandlingKind where 295 parseJSON (String "abort") = pure FailureHandlingAbort 296 parseJSON (String "transactional") = pure FailureHandlingTransactional 297 parseJSON (String "textOnlyTransactional") = pure FailureHandlingTextOnlyTransactional 298 parseJSON (String "undo") = pure FailureHandlingUndo 299 parseJSON _ = fail "FailureHandlingKind" 300 301data WorkspaceEditChangeAnnotationClientCapabilities = 302 WorkspaceEditChangeAnnotationClientCapabilities 303 { 304 -- | Whether the client groups edits with equal labels into tree nodes, 305 -- for instance all edits labelled with "Changes in Strings" would 306 -- be a tree node. 307 groupsOnLabel :: Maybe Bool 308 } deriving (Show, Read, Eq) 309 310deriveJSON lspOptions ''WorkspaceEditChangeAnnotationClientCapabilities 311 312data WorkspaceEditClientCapabilities = 313 WorkspaceEditClientCapabilities 314 { _documentChanges :: Maybe Bool -- ^The client supports versioned document 315 -- changes in 'WorkspaceEdit's 316 -- | The resource operations the client supports. Clients should at least 317 -- support @create@, @rename@ and @delete@ files and folders. 318 , _resourceOperations :: Maybe (List ResourceOperationKind) 319 -- | The failure handling strategy of a client if applying the workspace edit 320 -- fails. 321 , _failureHandling :: Maybe FailureHandlingKind 322 -- | Whether the client normalizes line endings to the client specific 323 -- setting. 324 -- 325 -- If set to `true` the client will normalize line ending characters 326 -- in a workspace edit to the client specific new line character(s). 327 -- 328 -- @since 3.16.0 329 , _normalizesLineEndings :: Maybe Bool 330 -- | Whether the client in general supports change annotations on text edits, 331 -- create file, rename file and delete file changes. 332 -- 333 -- @since 3.16.0 334 , _changeAnnotationSupport :: Maybe WorkspaceEditChangeAnnotationClientCapabilities 335 } deriving (Show, Read, Eq) 336 337deriveJSON lspOptions ''WorkspaceEditClientCapabilities 338 339-- --------------------------------------------------------------------- 340 341data ApplyWorkspaceEditParams = 342 ApplyWorkspaceEditParams 343 { -- | An optional label of the workspace edit. This label is 344 -- presented in the user interface for example on an undo 345 -- stack to undo the workspace edit. 346 _label :: Maybe Text 347 -- | The edits to apply 348 , _edit :: WorkspaceEdit 349 } deriving (Show, Read, Eq) 350 351deriveJSON lspOptions ''ApplyWorkspaceEditParams 352 353data ApplyWorkspaceEditResponseBody = 354 ApplyWorkspaceEditResponseBody 355 { -- | Indicates whether the edit was applied or not. 356 _applied :: Bool 357 -- | An optional textual description for why the edit was not applied. 358 -- This may be used may be used by the server for diagnostic 359 -- logging or to provide a suitable error for a request that 360 -- triggered the edit. 361 , _failureReason :: Maybe Text 362 } deriving (Show, Read, Eq) 363 364deriveJSON lspOptions ''ApplyWorkspaceEditResponseBody 365 366-- --------------------------------------------------------------------- 367 368-- | Applies a 'TextEdit' to some 'Text'. 369-- >>> applyTextEdit (TextEdit (Range (Position 0 1) (Position 0 2)) "i") "foo" 370-- "fio" 371applyTextEdit :: TextEdit -> Text -> Text 372applyTextEdit (TextEdit (Range sp ep) newText) oldText = 373 let (_, afterEnd) = splitAtPos ep oldText 374 (beforeStart, _) = splitAtPos sp oldText 375 in mconcat [beforeStart, newText, afterEnd] 376 where 377 splitAtPos :: Position -> Text -> (Text, Text) 378 splitAtPos (Position sl sc) t = 379 -- If we are looking for a line beyond the end of the text, this will give us an index 380 -- past the end. Fortunately, T.splitAt is fine with this, and just gives us the whole 381 -- string and an empty string, which is what we want. 382 let index = sc + startLineIndex sl t 383 in T.splitAt index t 384 385 -- The index of the first character of line 'line' 386 startLineIndex 0 _ = 0 387 startLineIndex line t' = 388 case T.findIndex (== '\n') t' of 389 Just i -> i + 1 + startLineIndex (line - 1) (T.drop (i + 1) t') 390 -- i != 0, and there are no newlines, so this is a line beyond the end of the text. 391 -- In this case give the "start index" as the end, so we will at least append the text. 392 Nothing -> T.length t' 393 394-- | 'editTextEdit' @outer@ @inner@ applies @inner@ to the text inside @outer@. 395editTextEdit :: TextEdit -> TextEdit -> TextEdit 396editTextEdit (TextEdit origRange origText) innerEdit = 397 let newText = applyTextEdit innerEdit origText 398 in TextEdit origRange newText 399