Skip to content

Commit

Permalink
Add a code action to create missing linked files (#239)
Browse files Browse the repository at this point in the history
  • Loading branch information
jparoz authored Jul 25, 2023
1 parent e9386bd commit 77f348c
Show file tree
Hide file tree
Showing 8 changed files with 163 additions and 16 deletions.
59 changes: 53 additions & 6 deletions LanguageServerProtocol/Types.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
[<ErasedUnion>]
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
Expand Down Expand Up @@ -1147,22 +1191,25 @@ 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) =
(capabilities.Workspace
|> 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 =
Expand Down
53 changes: 53 additions & 0 deletions Marksman/CodeActions.fs
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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 ->
Expand Down Expand Up @@ -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 }
}
15 changes: 15 additions & 0 deletions Marksman/Config.fs
Original file line number Diff line number Diff line change
Expand Up @@ -124,18 +124,21 @@ module TextSync =
/// without lenses manageable.
type Config =
{ caTocEnable: option<bool>
caCreateMissingFileEnable: option<bool>
coreMarkdownFileExtensions: option<array<string>>
coreTextSync: option<TextSync>
complWikiStyle: option<ComplWikiStyle> }

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 }
Expand All @@ -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
Expand All @@ -164,6 +172,9 @@ let private configOfTable (table: TomlTable) : LookupResult<Config> =
monad {
let! caTocEnable = getFromTableOpt<bool> table [] [ "code_action"; "toc"; "enable" ]

let! caCreateMissingFileEnable =
getFromTableOpt<bool> table [] [ "code_action"; "create_missing_file"; "enable" ]

let! coreMarkdownFileExtensions =
getFromTableOpt<array<string>> table [] [ "core"; "markdown"; "file_extensions" ]

Expand All @@ -176,6 +187,7 @@ let private configOfTable (table: TomlTable) : LookupResult<Config> =
complWikiStyle |> Option.bind ComplWikiStyle.ofStringOpt

{ caTocEnable = caTocEnable
caCreateMissingFileEnable = caCreateMissingFileEnable
coreMarkdownFileExtensions = coreMarkdownFileExtensions
coreTextSync = coreTextSync
complWikiStyle = complWikiStyle }
Expand All @@ -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
Expand Down
7 changes: 7 additions & 0 deletions Marksman/Misc.fs
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,13 @@ let chopMarkdownExt (configuredExts: seq<string>) (path: string) : string =
else
path

let ensureMarkdownExt (configuredExts: seq<string>) (path: string) : string =
if isMarkdownFile configuredExts path then
path
else
let ext = Seq.head configuredExts
$"{path}.{ext}"

let isPotentiallyMarkdownFile (configuredExts: seq<string>) (path: string) : bool =
let ext = Path.GetExtension path

Expand Down
6 changes: 4 additions & 2 deletions Marksman/Refactor.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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<TextEdit> =
Expand Down
22 changes: 18 additions & 4 deletions Marksman/Server.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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))

Expand Down
11 changes: 8 additions & 3 deletions Tests/RefactorTests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
6 changes: 5 additions & 1 deletion Tests/default.marksman.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down

0 comments on commit 77f348c

Please sign in to comment.