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