Skip to content

Commit 6bafc58

Browse files
authored
feat: Use core.titleFromHeading in parsing and semantic analysis (#369)
The setting is now used during parsing CST -> AST -> Syms transformation can now use 'isTitle' metadata on a heading node.
1 parent c303344 commit 6bafc58

8 files changed

Lines changed: 134 additions & 12 deletions

File tree

Marksman/Ast.fs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ open Marksman.Syms
66

77
type Heading = {
88
level: int
9+
isTitle: bool
910
text: string
1011
id: Slug
1112
} with
@@ -111,14 +112,13 @@ module Element =
111112
// TODO: instead of checking for 'all whitespace' symbols all the time, create smart constructors
112113
let toSym (parserSettings: Config.ParserSettings) (el: Element) : option<Sym> =
113114
match el with
114-
| Element.H { level = level; id = id } ->
115+
| Element.H { level = level; isTitle = isTitle; id = id } ->
115116
if Slug.isEmpty id then
116117
None
117118
else
118119
let id = Slug.toString id
119120

120-
// TODO: make this configurable
121-
if level <= 1 then
121+
if isTitle then
122122
Syms.Sym.Def(Title(id)) |> Some
123123
else
124124
Syms.Sym.Def(Header(level, id)) |> Some

Marksman/Cst.fs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,7 @@ type Element =
252252
| T n -> n.range
253253
| YML n -> n.range
254254

255-
and Heading = { level: int; title: TextNode; scope: Range }
255+
and Heading = { level: int; isTitle: bool; title: TextNode; scope: Range }
256256

257257
let rec private fmtElement =
258258
function
@@ -298,14 +298,15 @@ module Heading =
298298

299299
let slug (heading: Heading) : Slug = name heading |> Slug.ofString
300300

301-
let isTitle (heading: Heading) = heading.level <= 1
301+
let isTitle (heading: Heading) = heading.isTitle
302302

303303
let range (heading: Heading) : Range = heading.title.range
304304

305305
let scope (heading: Heading) : Range = heading.scope
306306

307307
let toAbstract (cHead: Heading) : Ast.Heading = {
308308
level = cHead.level
309+
isTitle = cHead.isTitle
309310
text = cHead.title.text
310311
id = slug cHead
311312
}

Marksman/Index.fs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ open Marksman.Misc
77

88
type Dictionary<'K, 'V> = System.Collections.Generic.Dictionary<'K, 'V>
99

10+
// TODO: get rid of this; use Structure directly
1011
type Index = {
1112
titles: array<Node<Heading>>
1213
headings: array<Node<Heading>>

Marksman/Parser.fs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ open System.Collections.Generic
55
open Ionide.LanguageServerProtocol.Types
66
open Markdig.Syntax
77

8+
open Marksman.Config
89
open Marksman.Misc
910
open Marksman.Names
1011
open Marksman.Text
@@ -216,7 +217,7 @@ module Markdown =
216217
}
217218

218219

219-
let scrapeText (text: Text) : array<Element> =
220+
let scrapeText (parserSettings: ParserSettings) (text: Text) : array<Element> =
220221
let parsed: MarkdownObject = Markdown.Parse(text.content, markdigPipeline)
221222

222223
let elements = ResizeArray()
@@ -250,6 +251,7 @@ module Markdown =
250251
let heading =
251252
Node.mk fullText range {
252253
level = level
254+
isTitle = parserSettings.titleFromHeading && level <= 1
253255
title = Node.mkText title titleRange
254256
scope = range
255257
}
@@ -476,11 +478,11 @@ module Markdown =
476478

477479
{ elements = elements; childMap = childMap }
478480

479-
let parse (parserSettings: Config.ParserSettings) (text: Text) : Structure =
481+
let parse (parserSettings: ParserSettings) (text: Text) : Structure =
480482
if String.IsNullOrEmpty text.content then
481483
let cst: Cst.Cst = { elements = [||]; childMap = Map.empty }
482484
Structure.ofCst parserSettings cst
483485
else
484-
let flatElements = Markdown.scrapeText text
486+
let flatElements = Markdown.scrapeText parserSettings text
485487
let cst = Markdown.buildCst text flatElements
486488
Structure.ofCst parserSettings cst

Tests/AstTests.fs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
module Marksman.AstTests
22

3+
open Marksman.Config
34
open Xunit
45
open Snapper
56

@@ -62,3 +63,51 @@ let testAstLookup () =
6263

6364
let madeUpAbstract = Element.MR(Collapsed "WAT")
6465
Assert.Equal(Structure.tryFindConcreteForAbstract madeUpAbstract struct1, None)
66+
67+
[<Fact>]
68+
let testSymsWhenTitleFromHeadingIsOff () =
69+
let doc =
70+
"""
71+
# H1
72+
Is this a title?
73+
# H2
74+
Is this another title?
75+
## H2.2
76+
# H3
77+
And this?
78+
"""
79+
80+
let strukt =
81+
Parser.parse { ParserSettings.Default with titleFromHeading = false } (Text.mkText doc)
82+
83+
Helpers.checkInlineSnapshot _.ToString() strukt.Symbols [
84+
"Doc"
85+
"H1 {h1}"
86+
"H1 {h2}"
87+
"H1 {h3}"
88+
"H2 {h22}"
89+
]
90+
91+
[<Fact>]
92+
let testSymsWhenTitleFromHeadingIsOn () =
93+
let doc =
94+
"""
95+
# H1
96+
Is this a title?
97+
# H2
98+
Is this another title?
99+
## H2.2
100+
# H3
101+
And this?
102+
"""
103+
104+
let strukt =
105+
Parser.parse { ParserSettings.Default with titleFromHeading = true } (Text.mkText doc)
106+
107+
Helpers.checkInlineSnapshot _.ToString() strukt.Symbols [
108+
"Doc"
109+
"T {h1}"
110+
"T {h2}"
111+
"T {h3}"
112+
"H2 {h22}"
113+
]

Tests/ConnTest.fs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
module Marksman.ConnTest
22

3+
open Marksman.Config
34
open Xunit
45

56
open Snapper
@@ -363,3 +364,35 @@ module ConnGraphTests =
363364
let d2 = FakeDoc.Mk(path = "d2.md", contentLines = [| "#tag2"; "#tag3" |])
364365
let f' = mkFolder [ d1; d2 ] |> Folder.withDoc d1'
365366
checkSnapshot (Folder.conn f')
367+
368+
[<StoreSnapshotsPerClass>]
369+
module ConnGraphTests_TitleLess =
370+
let incrConfig = {
371+
Config.Config.Default with
372+
coreIncrementalReferences = Some true
373+
coreTitleFromHeading = Some false
374+
complWikiStyle = Some ComplWikiStyle.TitleSlug
375+
}
376+
377+
let mkFolder docs = FakeFolder.Mk(config = incrConfig, docs = docs)
378+
let mkDoc path content = FakeDoc.Mk(path = path, config = incrConfig, contentLines = content)
379+
380+
[<Fact>]
381+
let updateH1 () =
382+
let d1 =
383+
mkDoc "ocaml.md" [| "# OCaml"; "[[#Multicore]]"; "## Multicore" |]
384+
385+
let d1' =
386+
mkDoc "ocaml.md" [| "# OCaml L"; "[[#Multicore]]"; "## Multicore" |]
387+
388+
let d2 = mkDoc "test.md" [| "# Test"; "[[OCaml#Multicore]]" |]
389+
390+
let incr =
391+
mkFolder [ d1 ]
392+
|> Folder.withDoc d2
393+
|> Folder.withDoc d1'
394+
|> Folder.conn
395+
396+
let fromScratch = mkFolder [ d1'; d2 ] |> Folder.conn
397+
let connDiff = Conn.difference fromScratch incr
398+
checkInlineSnapshot id [ connDiff.CompactFormat() ] [ "" ]

Tests/Helpers.fs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
module Marksman.Helpers
22

33
open System.Runtime.InteropServices
4+
open Marksman.Config
45
open Snapper
56
open Marksman.Misc
67
open Marksman.Paths
@@ -53,21 +54,22 @@ let stripMarginTrim (str: string) = stripMargin (str.Trim())
5354

5455
type FakeDoc =
5556
class
56-
static member Mk(content: string, ?path: string, ?root: string) : Doc =
57+
static member Mk(content: string, ?path: string, ?root: string, ?config: Config) : Doc =
5758
let text = Text.mkText content
5859
let path = defaultArg path "fake.md"
5960
let pathUri = pathToUri (dummyRootPath (pathComps path))
6061
let root = Option.map pathComps root |> Option.defaultValue []
6162
let rootUri = dummyRootPath root |> pathToUri
63+
let config = defaultArg config Config.Default
6264

6365
let docId =
6466
DocId(UriWith.mkRooted (UriWith.mkRoot rootUri) (LocalPath.ofUri pathUri))
6567

66-
Doc.mk Config.ParserSettings.Default docId None text
68+
Doc.mk (ParserSettings.OfConfig(config)) docId None text
6769

68-
static member Mk(contentLines: array<string>, ?path: string) : Doc =
70+
static member Mk(contentLines: array<string>, ?path: string, ?config: Config) : Doc =
6971
let content = String.concat System.Environment.NewLine contentLines
70-
FakeDoc.Mk(content, ?path = path)
72+
FakeDoc.Mk(content, ?path = path, ?config = config)
7173
end
7274

7375
type FakeFolder =

Tests/RefsTests.fs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ module Marksman.RefsTests
22

33
open Ionide.LanguageServerProtocol.Types
44
open System.IO
5+
open Marksman.Config
56
open Xunit
67

78
open Marksman.Cst
@@ -574,6 +575,39 @@ module EncodingTests =
574575
let refs = resolveAtPos doc1 14 5
575576
checkInlineSnapshot (fun x -> x.ToString()) refs [ "doc.3.with.dots" ]
576577

578+
// Cases where title_from_heading = false
579+
module TitleLess =
580+
[<Fact>]
581+
let refToH1 () =
582+
let baseConfig = {
583+
Config.Config.Default with
584+
complWikiStyle = Some ComplWikiStyle.TitleSlug
585+
}
586+
587+
let mkFolder config =
588+
let d1 =
589+
FakeDoc.Mk(path = "d1.md", config = config, contentLines = [| "# Doc1" |])
590+
591+
let d2 =
592+
// 012345
593+
FakeDoc.Mk(path = "d2.md", config = config, contentLines = [| "[[Doc1]]" |])
594+
595+
let folder = FakeFolder.Mk([ d1; d2 ], config = config)
596+
d1, d2, folder
597+
598+
// In title-less mode headings are not referenceable cross-doc
599+
let _, d2, folder =
600+
mkFolder { baseConfig with coreTitleFromHeading = Some false }
601+
602+
let el = requireElementAtPos d2 0 2
603+
Assert.Empty(Dest.tryResolveElement folder d2 el)
604+
605+
// In title-full mode headings *are* referenceable cross-doc
606+
let _, d2, folder =
607+
mkFolder { baseConfig with coreTitleFromHeading = Some true }
608+
609+
Assert.NotEmpty(Dest.tryResolveElement folder d2 el)
610+
577611
module RegressionTests =
578612
[<Fact>]
579613
let rootLink_issue275 () =

0 commit comments

Comments
 (0)