Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a code action to create nonexistent linked files #239

Merged
merged 3 commits into from
Jul 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"`.
jparoz marked this conversation as resolved.
Show resolved Hide resolved
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