diff --git a/LanguageServerProtocol/Types.fs b/LanguageServerProtocol/Types.fs index eed4b18..e659c94 100644 --- a/LanguageServerProtocol/Types.fs +++ b/LanguageServerProtocol/Types.fs @@ -236,6 +236,50 @@ type TextDocumentEdit = /// The edits to be applied. Edits: TextEdit [] } +/// Create file operation +type CreateFile = + { /// The kind of resource operation. This should always be `"create"`. + Kind: string + + /// The resource to create. + Uri: DocumentUri + } + +/// Rename file operation +type RenameFile = + { /// The kind of resource operation. This should always be `"rename"`. + Kind: string + + /// The old (existing) location. + oldUri: DocumentUri + + /// The new location. + newUri: DocumentUri + } + +/// Delete file operation +type DeleteFile = + { /// The kind of resource operation. This should always be `"delete"`. + Kind: string + + /// The file to delete. + Uri: DocumentUri + } + + +/// Represents the possible values in the `WorkspaceEdit`'s `DocumentChanges` field. +[] +type DocumentChange = + | TextDocumentEdit of TextDocumentEdit + | CreateFile of CreateFile + | RenameFile of RenameFile + | DeleteFile of DeleteFile + +module DocumentChange = + let createFile uri = DocumentChange.CreateFile {Kind = "create"; Uri = uri} + let renameFile oldUri newUri = DocumentChange.RenameFile {Kind = "rename"; oldUri = oldUri; newUri = newUri} + let deleteFile uri = DocumentChange.DeleteFile {Kind = "delete"; Uri = uri} + type TraceSetting = | Off = 0 | Messages = 1 @@ -1147,10 +1191,13 @@ type WorkspaceEdit = /// where each text document edit addresses a specific version of a text document. /// Whether a client supports versioned document edits is expressed via /// `WorkspaceClientCapabilities.workspaceEdit.documentChanges`. - DocumentChanges: TextDocumentEdit [] option } - static member DocumentChangesToChanges(edits: TextDocumentEdit []) = + DocumentChanges: DocumentChange [] option } + static member DocumentChangesToChanges(edits: DocumentChange []) = edits - |> Array.map (fun edit -> edit.TextDocument.Uri.ToString(), edit.Edits) + |> Array.collect (fun docChange -> + match docChange with + | TextDocumentEdit edit -> [|edit.TextDocument.Uri.ToString(), edit.Edits|] + | _ -> [||]) |> Map.ofArray static member CanUseDocumentChanges(capabilities: ClientCapabilities) = @@ -1158,11 +1205,11 @@ type WorkspaceEdit = |> Option.bind (fun x -> x.WorkspaceEdit) |> Option.bind (fun x -> x.DocumentChanges)) = Some true - static member Create(edits: TextDocumentEdit [], capabilities: ClientCapabilities) = + static member Create(documentChanges: DocumentChange [], capabilities: ClientCapabilities) = if WorkspaceEdit.CanUseDocumentChanges(capabilities) then - { Changes = None; DocumentChanges = Some edits } + { Changes = None; DocumentChanges = Some documentChanges } else - { Changes = Some(WorkspaceEdit.DocumentChangesToChanges edits) + { Changes = Some(WorkspaceEdit.DocumentChangesToChanges documentChanges) DocumentChanges = None } type MessageType = diff --git a/Marksman/CodeActions.fs b/Marksman/CodeActions.fs index b3763f6..75a2759 100644 --- a/Marksman/CodeActions.fs +++ b/Marksman/CodeActions.fs @@ -1,11 +1,16 @@ module Marksman.CodeActions +open FSharpPlus open Ionide.LanguageServerProtocol.Types open Ionide.LanguageServerProtocol.Logging open Marksman.Toc open Marksman.Workspace open Marksman.Misc +open Marksman.Refs +open Marksman.Index +open Marksman.Names +open Marksman.Paths open type System.Environment @@ -20,6 +25,13 @@ let documentEdit range text documentUri : WorkspaceEdit = { Changes = Some workspaceChanges; DocumentChanges = None } +type CreateFileAction = { name: string; newFileUri: DocumentUri } + +let createFile newFileUri : WorkspaceEdit = + let documentChanges = [| DocumentChange.createFile newFileUri |] + + { Changes = None; DocumentChanges = Some documentChanges } + let tableOfContentsInner (doc: Doc) : DocumentAction option = match TableOfContents.mk (Doc.index doc) with | Some toc -> @@ -99,3 +111,44 @@ let tableOfContents (doc: Doc) : DocumentAction option = tableOfContentsInner doc + +let createMissingFile + (range: Range) + (_context: CodeActionContext) + (doc: Doc) + (folder: Folder) + : CreateFileAction option = + let configuredExts = + (Folder.configOrDefault folder).CoreMarkdownFileExtensions() + + let pos = range.Start + + monad' { + let! atPos = Doc.index doc |> Index.linkAtPos pos + let! uref = Uref.ofElement configuredExts (Doc.id doc) atPos + let refs = Dest.tryResolveUref uref doc folder + + // Early return if the file exists + do! guard (Seq.isEmpty refs) + + let! name = + match uref with + | Uref.Doc name -> Some name.data + | Uref.Heading (Some name, _) -> Some name.data + | _ -> None + + let! internPath = InternName.tryAsPath name + + let relPath = + InternPath.toRel internPath + |> RelPath.toSystem + |> ensureMarkdownExt configuredExts + |> RelPath + + let path = RootPath.append (Folder.rootPath folder) relPath + let filename = AbsPath.filename path + let uri = AbsPath.toUri path + + // create the file + { name = $"Create `{filename}`"; newFileUri = uri } + } diff --git a/Marksman/Config.fs b/Marksman/Config.fs index e35efd9..aa400df 100644 --- a/Marksman/Config.fs +++ b/Marksman/Config.fs @@ -124,18 +124,21 @@ module TextSync = /// without lenses manageable. type Config = { caTocEnable: option + caCreateMissingFileEnable: option coreMarkdownFileExtensions: option> coreTextSync: option complWikiStyle: option } static member Default = { caTocEnable = Some true + caCreateMissingFileEnable = Some true coreMarkdownFileExtensions = Some [| "md"; "markdown" |] coreTextSync = Some Full complWikiStyle = Some TitleSlug } static member Empty = { caTocEnable = None + caCreateMissingFileEnable = None coreMarkdownFileExtensions = None coreTextSync = None complWikiStyle = None } @@ -145,6 +148,11 @@ type Config = |> Option.orElse Config.Default.caTocEnable |> Option.get + member this.CaCreateMissingFileEnable() = + this.caCreateMissingFileEnable + |> Option.orElse Config.Default.caCreateMissingFileEnable + |> Option.get + member this.CoreMarkdownFileExtensions() = this.coreMarkdownFileExtensions |> Option.orElse Config.Default.coreMarkdownFileExtensions @@ -164,6 +172,9 @@ let private configOfTable (table: TomlTable) : LookupResult = monad { let! caTocEnable = getFromTableOpt table [] [ "code_action"; "toc"; "enable" ] + let! caCreateMissingFileEnable = + getFromTableOpt table [] [ "code_action"; "create_missing_file"; "enable" ] + let! coreMarkdownFileExtensions = getFromTableOpt> table [] [ "core"; "markdown"; "file_extensions" ] @@ -176,6 +187,7 @@ let private configOfTable (table: TomlTable) : LookupResult = complWikiStyle |> Option.bind ComplWikiStyle.ofStringOpt { caTocEnable = caTocEnable + caCreateMissingFileEnable = caCreateMissingFileEnable coreMarkdownFileExtensions = coreMarkdownFileExtensions coreTextSync = coreTextSync complWikiStyle = complWikiStyle } @@ -186,6 +198,9 @@ module Config = let merge hi low = { caTocEnable = hi.caTocEnable |> Option.orElse low.caTocEnable + caCreateMissingFileEnable = + hi.caCreateMissingFileEnable + |> Option.orElse low.caCreateMissingFileEnable coreMarkdownFileExtensions = hi.coreMarkdownFileExtensions |> Option.orElse low.coreMarkdownFileExtensions diff --git a/Marksman/Misc.fs b/Marksman/Misc.fs index 03b2c45..7010e3a 100644 --- a/Marksman/Misc.fs +++ b/Marksman/Misc.fs @@ -153,6 +153,13 @@ let chopMarkdownExt (configuredExts: seq) (path: string) : string = else path +let ensureMarkdownExt (configuredExts: seq) (path: string) : string = + if isMarkdownFile configuredExts path then + path + else + let ext = Seq.head configuredExts + $"{path}.{ext}" + let isPotentiallyMarkdownFile (configuredExts: seq) (path: string) : bool = let ext = Path.GetExtension path diff --git a/Marksman/Refactor.fs b/Marksman/Refactor.fs index dcfc18a..fec3358 100644 --- a/Marksman/Refactor.fs +++ b/Marksman/Refactor.fs @@ -48,10 +48,12 @@ let mkWorkspaceEdit for docEdit in docEdits do docEdit.Edits |> Array.sortInPlaceWith (fun x y -> -(compare x y)) + let docChanges = docEdits |> Array.map (DocumentChange.TextDocumentEdit) + if supportsDocumentEdit then - { Changes = None; DocumentChanges = Some docEdits } + { Changes = None; DocumentChanges = Some docChanges } else - { Changes = Some(WorkspaceEdit.DocumentChangesToChanges docEdits) + { Changes = Some(WorkspaceEdit.DocumentChangesToChanges docChanges) DocumentChanges = None } let renameMarkdownLabel (newLabel: string) (element: Element) : option = diff --git a/Marksman/Server.fs b/Marksman/Server.fs index 9537018..2c54d16 100644 --- a/Marksman/Server.fs +++ b/Marksman/Server.fs @@ -922,9 +922,9 @@ type MarksmanServer(client: MarksmanClient) = <| fun state -> let docPath = opts.TextDocument.Uri |> UriWith.mkAbs - let codeAction title edit = + let codeAction title kind edit = { Title = title - Kind = Some CodeActionKind.Source + Kind = kind Diagnostics = None Command = None Data = None @@ -945,12 +945,26 @@ type MarksmanServer(client: MarksmanClient) = let wsEdit = (CodeActions.documentEdit ca.edit ca.newText opts.TextDocument.Uri) - codeAction ca.name wsEdit) + let caKind = Some CodeActionKind.Source + + codeAction ca.name caKind wsEdit) + else + [||] + + let createMissingFileAction = + if config.CaCreateMissingFileEnable() then + CodeActions.createMissingFile opts.Range opts.Context doc folder + |> Option.toArray + |> Array.map (fun ca -> + let wsEdit = CodeActions.createFile ca.newFileUri + let caKind = Some CodeActionKind.QuickFix + codeAction ca.name caKind wsEdit) else [||] let codeActions: TextDocumentCodeActionResult = - tocAction |> Array.map U2.Second + Array.concat [| tocAction; createMissingFileAction |] + |> Array.map U2.Second Mutation.output (LspResult.success (Some codeActions)) diff --git a/Tests/RefactorTests.fs b/Tests/RefactorTests.fs index ecf42d4..d847842 100644 --- a/Tests/RefactorTests.fs +++ b/Tests/RefactorTests.fs @@ -9,9 +9,14 @@ let editRanges = function | Refactor.Edit wsEdit -> match wsEdit.DocumentChanges with - | Some docEdits -> - docEdits - |> Array.map (fun docEdit -> + | Some docChanges -> + docChanges + |> Array.map (fun docChange -> + let docEdit = + match docChange with + | TextDocumentEdit docEdit -> docEdit + | _ -> failwith $"Refactoring should always produce TextDocumentEdits" + let doc = Path.GetFileName(docEdit.TextDocument.Uri) let ranges = docEdit.Edits |> Array.map (fun x -> x.Range) doc, ranges) diff --git a/Tests/default.marksman.toml b/Tests/default.marksman.toml index 16d8a05..3f51ec1 100644 --- a/Tests/default.marksman.toml +++ b/Tests/default.marksman.toml @@ -7,7 +7,11 @@ markdown.file_extensions = ["md", "markdown"] text_sync = "full" [code_action] -toc.enable = true # Enable/disable "Table of Contents" code action +# Enable/disable "Table of Contents" code action +toc.enable = true + +# Enable/disable "Create missing linked file" code action +create_missing_file.enable = true [completion] # The style of wiki links completion.