@cosense-toolbox/lsp 0.1.0-beta.2 → 0.1.0-beta.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -5
- package/dist/check.d.mts +1 -1
- package/dist/check.d.mts.map +1 -1
- package/dist/check.mjs +7 -13
- package/dist/check.mjs.map +1 -1
- package/dist/completion.d.mts +4 -1
- package/dist/completion.d.mts.map +1 -1
- package/dist/completion.mjs +9 -3
- package/dist/completion.mjs.map +1 -1
- package/dist/link.d.mts +37 -0
- package/dist/link.d.mts.map +1 -0
- package/dist/link.mjs +91 -0
- package/dist/link.mjs.map +1 -0
- package/dist/main.mjs +1 -1
- package/dist/{server-DPPxtJ6Y.mjs → server-B7AZxbRw.mjs} +37 -10
- package/dist/server-B7AZxbRw.mjs.map +1 -0
- package/dist/tokens.d.mts +13 -8
- package/dist/tokens.d.mts.map +1 -1
- package/dist/tokens.mjs +283 -2
- package/dist/tokens.mjs.map +1 -0
- package/dist/tree-sXzD2703.mjs +29 -0
- package/dist/tree-sXzD2703.mjs.map +1 -0
- package/dist/{workspace-DouAOZnK.mjs → workspace-A-W0PU0g.mjs} +5 -43
- package/dist/workspace-A-W0PU0g.mjs.map +1 -0
- package/package.json +8 -3
- package/dist/server-DPPxtJ6Y.mjs.map +0 -1
- package/dist/tokens-D0NP_uLo.mjs +0 -312
- package/dist/tokens-D0NP_uLo.mjs.map +0 -1
- package/dist/workspace-DouAOZnK.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -21,7 +21,6 @@ csn-lsp --stdio
|
|
|
21
21
|
| 設定 | 内容 | 既定 |
|
|
22
22
|
| :---------------- | :------------------------------------------------------------------------------------- | :----------------- |
|
|
23
23
|
| `sources` | ページを読む場所。ワークスペースからの相対パス | ワークスペース全体 |
|
|
24
|
-
| `decorations` | サイトの `customDecorations` と同じ記号。`[! 注意]` をリンクではなく装飾記法として読む | なし |
|
|
25
24
|
| `unresolvedLinks` | 存在しないページへのリンクの診断。`off` / `hint` / `information` / `warning` / `error` | `warning` |
|
|
26
25
|
| `frontmatter` | 1 行目の `---` を frontmatter (YAML) として飛ばすか | `true` |
|
|
27
26
|
|
|
@@ -30,18 +29,18 @@ csn-lsp --stdio
|
|
|
30
29
|
エディタの診断と同じ判定で、サイトのすべてのページのリンクを調べる。CI で使う想定。
|
|
31
30
|
|
|
32
31
|
```sh
|
|
33
|
-
csn-lsp check src/content src/pages
|
|
32
|
+
csn-lsp check src/content src/pages
|
|
34
33
|
# src/content/posts/a.csn:5:3 error リンク先のページが見つからない: [無いページ]
|
|
35
34
|
```
|
|
36
35
|
|
|
37
36
|
- 引数のディレクトリの下の `.csn` / `.csnx` を読む。省略すると今いるディレクトリを読む
|
|
38
37
|
- ページの題名はファイルの 1 行目。大文字小文字と、空白と `_` の違いは無視する
|
|
38
|
+
- `[! 注意]` のような Cosense の文字装飾の記号の括弧は、Cosense Web と同じく装飾として読み、リンクとして調べない
|
|
39
39
|
- エラーが 1 件でもあれば終了コード 1、無ければ 0。オプションの誤りは 2
|
|
40
40
|
|
|
41
41
|
| オプション | 内容 | 既定 |
|
|
42
42
|
| :--------------------------- | :--------------------------------------------------- | :------ |
|
|
43
43
|
| `--unresolved-links <level>` | `off` / `hint` / `information` / `warning` / `error` | `error` |
|
|
44
|
-
| `--decorations <markers>` | サイトの装飾記法の記号を 1 つの文字列で (`'\|!~#'`) | なし |
|
|
45
44
|
| `--no-frontmatter` | 1 行目の `---` を frontmatter ではなく題名として読む | |
|
|
46
45
|
|
|
47
46
|
`check` の既定が `error` なのは、ビルドを止めるための道具だから。報告だけにしたいときは
|
|
@@ -51,7 +50,17 @@ csn-lsp check src/content src/pages --decorations '|!~#'
|
|
|
51
50
|
|
|
52
51
|
エディタのプラグインなどから、部品として使える。
|
|
53
52
|
|
|
54
|
-
- `@cosense-toolbox/lsp/tokens`: `computeTokens` / `encodeTokens` / `legendOf`
|
|
55
|
-
- `notations`
|
|
53
|
+
- `@cosense-toolbox/lsp/tokens`: `computeTokens` / `encodeTokens` / `legendOf` など
|
|
54
|
+
- `notations` で装飾の記号に名前を付け、その名前のトークンとして送れる (`[! 注意]` の `!` に `warning` など)。
|
|
55
|
+
記号は Cosense の文字装飾の記号 (`!"#%&'()*+,-./{|}<>_~=`) に限る。ほかの記号 (`@` など) の括弧はリンクのまま
|
|
56
56
|
- `frontmatter: false` で、1 行目の `---` を YAML として飛ばさずに読める
|
|
57
|
+
- `encodeTokens` は、legend にトークン自身の型名 (`title`、`link` など) か notation の名前があればその名前で送り、
|
|
58
|
+
無ければ LSP 標準の型 (`namespace`、`function` など) に直して送る。既定の legend (`LEGEND`) は LSP 標準の型だけなので、
|
|
59
|
+
VS Code や Zed のように標準の型で色を付けるエディタにはそのまま渡せる。自前の名前で色を付けるクライアントは、
|
|
60
|
+
`encodeTokens(tokens, [...TOKEN_TYPES, ...names])` のように自前の legend を渡す
|
|
57
61
|
- `@cosense-toolbox/lsp/completion`: `detectCompletion` / `completionItems` / `definitionOf` など
|
|
62
|
+
- `@cosense-toolbox/lsp/link`: `linkAt(text, position, parseOptions?)` で、カーソルの下のリンクと、その行き先を返す
|
|
63
|
+
- ページ (`[ページ]`、`#タグ`、`[/project/ページ]`、アイコンの `[taro.icon]` と `[[taro.icon]]`) は `kind: "page"`。
|
|
64
|
+
別のプロジェクトなら `project`、`[ページ#<行 ID>]` なら `lineId` が付く
|
|
65
|
+
- 外部リンクと画像は `kind: "url"`。リンク付きの画像は、画像ではなくリンクの URL
|
|
66
|
+
- `range` は記法全体の範囲 (UTF-16)。行き先をファイルや Web のページに解決するのは、呼び出し側の仕事
|
package/dist/check.d.mts
CHANGED
|
@@ -19,7 +19,7 @@ type UnresolvedSeverity = typeof UnresolvedSeverity.Type;
|
|
|
19
19
|
*
|
|
20
20
|
* The same functions decide as in the editor, so what the editor flags is what fails here.
|
|
21
21
|
*/
|
|
22
|
-
declare const USAGE = "Usage: csn-lsp check [directory ...] [options]\n\nReads the .csn and .csnx pages under each directory (default: the current one) and reports\nevery [link] to a page none of them holds.\n\nOptions:\n --unresolved-links <level> off | hint | information | warning | error (default: error)\n --
|
|
22
|
+
declare const USAGE = "Usage: csn-lsp check [directory ...] [options]\n\nReads the .csn and .csnx pages under each directory (default: the current one) and reports\nevery [link] to a page none of them holds.\n\nOptions:\n --unresolved-links <level> off | hint | information | warning | error (default: error)\n --no-frontmatter read a first line of --- as the title, not as YAML\n";
|
|
23
23
|
/** What the command prints, and the code it exits with. */
|
|
24
24
|
interface CheckResult {
|
|
25
25
|
readonly output: string;
|
package/dist/check.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"check.d.mts","names":[],"sources":["../src/diagnostics.ts","../src/check.ts"],"mappings":";;;;;;;;;;;;cAsBa,oBAAkB,OAAA;KAEnB,4BAA4B,mBAAmB;;;;;;;;
|
|
1
|
+
{"version":3,"file":"check.d.mts","names":[],"sources":["../src/diagnostics.ts","../src/check.ts"],"mappings":";;;;;;;;;;;;cAsBa,oBAAkB,OAAA;KAEnB,4BAA4B,mBAAmB;;;;;;;;cCR9C;;UAWI;WACN;WACA;;;UAIM;;WAEN,OAAO;;WAEP,iBAAiB;;WAEjB,eAAe;;WAEf;;KAGC;;UAGK;WACN;WACA;WACA;WACA,OAAO;WACP;;;;;;cAyDE,cAAa,OAAA,iBAAA,cAAA,eAKvB,qBAAmB,OAAO,OAAO,cAAc;;cAuBrC,WAAY,MAAM,uBAAuB,gBAAc,OAAO,OAAO"}
|
package/dist/check.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { i as readPageFiles,
|
|
2
|
-
import { Array
|
|
1
|
+
import { i as readPageFiles, n as indexOf, o as severityOf, s as unresolvedLinkDiagnostics } from "./workspace-A-W0PU0g.mjs";
|
|
2
|
+
import { Array, Effect, Match, Option, Predicate, pipe } from "effect";
|
|
3
3
|
import { DiagnosticSeverity } from "vscode-languageserver/node";
|
|
4
4
|
import { relative, resolve, sep } from "node:path";
|
|
5
5
|
import { parseArgs } from "node:util";
|
|
@@ -16,7 +16,6 @@ every [link] to a page none of them holds.
|
|
|
16
16
|
|
|
17
17
|
Options:
|
|
18
18
|
--unresolved-links <level> off | hint | information | warning | error (default: error)
|
|
19
|
-
--decorations <markers> the site's own decoration markers, as one string: '|!~#'
|
|
20
19
|
--no-frontmatter read a first line of --- as the title, not as YAML
|
|
21
20
|
`;
|
|
22
21
|
/**
|
|
@@ -30,22 +29,17 @@ const argsOf = (args, cwd) => pipe(Option.liftThrowable(parseArgs)({
|
|
|
30
29
|
strict: true,
|
|
31
30
|
options: {
|
|
32
31
|
"unresolved-links": { type: "string" },
|
|
33
|
-
decorations: { type: "string" },
|
|
34
32
|
"no-frontmatter": { type: "boolean" }
|
|
35
33
|
}
|
|
36
34
|
}), Option.map(({ values, positionals }) => ({
|
|
37
|
-
roots: Array
|
|
35
|
+
roots: Array.match(positionals, {
|
|
38
36
|
onEmpty: () => [cwd],
|
|
39
|
-
onNonEmpty: (directories) => Array
|
|
37
|
+
onNonEmpty: (directories) => Array.map(directories, (directory) => resolve(cwd, directory))
|
|
40
38
|
}),
|
|
41
39
|
unresolvedLinks: pipe(Option.fromNullable(values["unresolved-links"]), Option.match({
|
|
42
40
|
onNone: () => DEFAULT_LEVEL,
|
|
43
41
|
onSome: severityOf
|
|
44
42
|
})),
|
|
45
|
-
parseOptions: parseOptionsOf({
|
|
46
|
-
...defaultSettings,
|
|
47
|
-
decorations: Array.from(values.decorations ?? "")
|
|
48
|
-
}),
|
|
49
43
|
frontmatter: values["no-frontmatter"] !== true
|
|
50
44
|
})));
|
|
51
45
|
const levelOf = (severity) => Match.value(severity).pipe(Match.when(DiagnosticSeverity.Error, () => "error"), Match.when(DiagnosticSeverity.Warning, () => "warning"), Match.when(DiagnosticSeverity.Information, () => "information"), Match.orElse(() => "hint"));
|
|
@@ -63,7 +57,7 @@ const reportOf = (file) => ({ range, severity, message }) => ({
|
|
|
63
57
|
*/
|
|
64
58
|
const checkSite = ({ roots, unresolvedLinks, parseOptions = {}, frontmatter = true }) => Effect.map(readPageFiles(roots, { frontmatter }), (files) => {
|
|
65
59
|
const index = indexOf(files);
|
|
66
|
-
return Array
|
|
60
|
+
return Array.flatMap(files, (file) => Array.map(unresolvedLinkDiagnostics(index, file.text, {
|
|
67
61
|
severity: unresolvedLinks,
|
|
68
62
|
components: file.path.endsWith(".csnx"),
|
|
69
63
|
parseOptions,
|
|
@@ -79,8 +73,8 @@ const runCheck = (args, cwd) => Option.match(argsOf(args, cwd), {
|
|
|
79
73
|
exitCode: 2
|
|
80
74
|
}),
|
|
81
75
|
onSome: (options) => Effect.map(checkSite(options), (reports) => ({
|
|
82
|
-
output: Array
|
|
83
|
-
exitCode: Array
|
|
76
|
+
output: Array.map(reports, lineOf(cwd)).join(""),
|
|
77
|
+
exitCode: Array.some(reports, ({ level }) => level === "error") ? 1 : 0
|
|
84
78
|
}))
|
|
85
79
|
});
|
|
86
80
|
//#endregion
|
package/dist/check.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"check.mjs","names":["Arr"],"sources":["../src/check.ts"],"sourcesContent":["import { relative, resolve, sep } from \"node:path\"\nimport { parseArgs } from \"node:util\"\n\nimport type { ParseOptions } from \"@cosense-toolbox/parser\"\nimport { Array as Arr, Effect, Match, Option, pipe, Predicate } from \"effect\"\nimport { type Diagnostic, DiagnosticSeverity } from \"vscode-languageserver/node\"\n\nimport { severityOf, unresolvedLinkDiagnostics, type UnresolvedSeverity } from \"./diagnostics\"\nimport {
|
|
1
|
+
{"version":3,"file":"check.mjs","names":["Arr"],"sources":["../src/check.ts"],"sourcesContent":["import { relative, resolve, sep } from \"node:path\"\nimport { parseArgs } from \"node:util\"\n\nimport type { ParseOptions } from \"@cosense-toolbox/parser\"\nimport { Array as Arr, Effect, Match, Option, pipe, Predicate } from \"effect\"\nimport { type Diagnostic, DiagnosticSeverity } from \"vscode-languageserver/node\"\n\nimport { severityOf, unresolvedLinkDiagnostics, type UnresolvedSeverity } from \"./diagnostics\"\nimport { indexOf, type PageFile, readPageFiles } from \"./workspace\"\n\n/**\n * `csn-lsp check`: the server's diagnostics for every page of a site, for a terminal or CI.\n *\n * The same functions decide as in the editor, so what the editor flags is what fails here.\n */\n\nexport const USAGE = `Usage: csn-lsp check [directory ...] [options]\n\nReads the .csn and .csnx pages under each directory (default: the current one) and reports\nevery [link] to a page none of them holds.\n\nOptions:\n --unresolved-links <level> off | hint | information | warning | error (default: error)\n --no-frontmatter read a first line of --- as the title, not as YAML\n`\n\n/** What the command prints, and the code it exits with. */\nexport interface CheckResult {\n readonly output: string\n readonly exitCode: number\n}\n\n/** Where to look and how to read, for `checkSite`. */\nexport interface CheckSiteOptions {\n /** The directories whose `.csn` / `.csnx` pages are read, and checked against each other. */\n readonly roots: ReadonlyArray<string>\n /** How a link to a missing page is reported; `off` reports none. */\n readonly unresolvedLinks: UnresolvedSeverity\n /** How to parse: a site's notation extensions, as its build parses with. */\n readonly parseOptions?: ParseOptions\n /** Whether a `---` fence on a page's first line opens YAML to skip (default: true). */\n readonly frontmatter?: boolean\n}\n\nexport type ReportLevel = \"error\" | \"warning\" | \"information\" | \"hint\"\n\n/** One thing found: where, counting lines and columns from 1, how loudly, and what. */\nexport interface Report {\n readonly path: string\n readonly line: number\n readonly column: number\n readonly level: ReportLevel\n readonly message: string\n}\n\n/**\n * Reports are errors unless told otherwise: the command exists to stop a build, and a\n * warning that passes would not.\n */\nconst DEFAULT_LEVEL: UnresolvedSeverity = \"error\"\n\nconst argsOf = (args: ReadonlyArray<string>, cwd: string): Option.Option<CheckSiteOptions> =>\n pipe(\n Option.liftThrowable(parseArgs)({\n args: [...args],\n allowPositionals: true,\n strict: true,\n options: {\n \"unresolved-links\": { type: \"string\" },\n \"no-frontmatter\": { type: \"boolean\" },\n },\n }),\n Option.map(({ values, positionals }) => ({\n roots: Arr.match(positionals, {\n onEmpty: () => [cwd],\n onNonEmpty: (directories) => Arr.map(directories, (directory) => resolve(cwd, directory)),\n }),\n unresolvedLinks: pipe(\n Option.fromNullable(values[\"unresolved-links\"]),\n Option.match({ onNone: () => DEFAULT_LEVEL, onSome: severityOf }),\n ),\n frontmatter: values[\"no-frontmatter\"] !== true,\n })),\n )\n\nconst levelOf = (severity: DiagnosticSeverity | undefined): ReportLevel =>\n Match.value(severity).pipe(\n Match.when(DiagnosticSeverity.Error, (): ReportLevel => \"error\"),\n Match.when(DiagnosticSeverity.Warning, (): ReportLevel => \"warning\"),\n Match.when(DiagnosticSeverity.Information, (): ReportLevel => \"information\"),\n Match.orElse((): ReportLevel => \"hint\"),\n )\n\n/** A file's diagnostic as a report, counting from 1 where the LSP counts from 0. */\nconst reportOf =\n (file: PageFile) =>\n ({ range, severity, message }: Diagnostic): Report => ({\n path: file.path,\n line: range.start.line + 1,\n column: range.start.character + 1,\n level: levelOf(severity),\n // The LSP lets a message be markup; a report is text either way.\n message: Predicate.isString(message) ? message : message.value,\n })\n\n/**\n * Every link to a missing page under `roots`, file by file in the order they were read.\n * The judgement is the editor's own, so what an editor flags is what is reported here.\n */\nexport const checkSite = ({\n roots,\n unresolvedLinks,\n parseOptions = {},\n frontmatter = true,\n}: CheckSiteOptions): Effect.Effect<ReadonlyArray<Report>> =>\n Effect.map(readPageFiles(roots, { frontmatter }), (files) => {\n const index = indexOf(files)\n return Arr.flatMap(files, (file) =>\n Arr.map(\n unresolvedLinkDiagnostics(index, file.text, {\n severity: unresolvedLinks,\n components: file.path.endsWith(\".csnx\"),\n parseOptions,\n frontmatter,\n }),\n reportOf(file),\n ),\n )\n })\n\n/** A report as a line of output: `path:line:column level message`, `path` from `cwd`. */\nconst lineOf =\n (cwd: string) =>\n ({ path, line, column, level, message }: Report): string =>\n `${relative(cwd, path).split(sep).join(\"/\")}:${line}:${column} ${level} ${message}\\n`\n\n/** Runs a check as `args` asks, from `cwd`. Never fails: a bad option is exit code 2. */\nexport const runCheck = (args: ReadonlyArray<string>, cwd: string): Effect.Effect<CheckResult> =>\n Option.match(argsOf(args, cwd), {\n onNone: () => Effect.succeed({ output: USAGE, exitCode: 2 }),\n onSome: (options) =>\n Effect.map(checkSite(options), (reports) => ({\n output: Arr.map(reports, lineOf(cwd)).join(\"\"),\n exitCode: Arr.some(reports, ({ level }) => level === \"error\") ? 1 : 0,\n })),\n })\n"],"mappings":";;;;;;;;;;;AAgBA,MAAa,QAAQ;;;;;;;;;;;;;AA2CrB,MAAM,gBAAoC;AAE1C,MAAM,UAAU,MAA6B,QAC3C,KACE,OAAO,cAAc,SAAS,CAAC,CAAC;CAC9B,MAAM,CAAC,GAAG,IAAI;CACd,kBAAkB;CAClB,QAAQ;CACR,SAAS;EACP,oBAAoB,EAAE,MAAM,SAAS;EACrC,kBAAkB,EAAE,MAAM,UAAU;CACtC;AACF,CAAC,GACD,OAAO,KAAK,EAAE,QAAQ,mBAAmB;CACvC,OAAOA,MAAI,MAAM,aAAa;EAC5B,eAAe,CAAC,GAAG;EACnB,aAAa,gBAAgBA,MAAI,IAAI,cAAc,cAAc,QAAQ,KAAK,SAAS,CAAC;CAC1F,CAAC;CACD,iBAAiB,KACf,OAAO,aAAa,OAAO,mBAAmB,GAC9C,OAAO,MAAM;EAAE,cAAc;EAAe,QAAQ;CAAW,CAAC,CAClE;CACA,aAAa,OAAO,sBAAsB;AAC5C,EAAE,CACJ;AAEF,MAAM,WAAW,aACf,MAAM,MAAM,QAAQ,CAAC,CAAC,KACpB,MAAM,KAAK,mBAAmB,aAA0B,OAAO,GAC/D,MAAM,KAAK,mBAAmB,eAA4B,SAAS,GACnE,MAAM,KAAK,mBAAmB,mBAAgC,aAAa,GAC3E,MAAM,aAA0B,MAAM,CACxC;;AAGF,MAAM,YACH,UACA,EAAE,OAAO,UAAU,eAAmC;CACrD,MAAM,KAAK;CACX,MAAM,MAAM,MAAM,OAAO;CACzB,QAAQ,MAAM,MAAM,YAAY;CAChC,OAAO,QAAQ,QAAQ;CAEvB,SAAS,UAAU,SAAS,OAAO,IAAI,UAAU,QAAQ;AAC3D;;;;;AAMF,MAAa,aAAa,EACxB,OACA,iBACA,eAAe,CAAC,GAChB,cAAc,WAEd,OAAO,IAAI,cAAc,OAAO,EAAE,YAAY,CAAC,IAAI,UAAU;CAC3D,MAAM,QAAQ,QAAQ,KAAK;CAC3B,OAAOA,MAAI,QAAQ,QAAQ,SACzBA,MAAI,IACF,0BAA0B,OAAO,KAAK,MAAM;EAC1C,UAAU;EACV,YAAY,KAAK,KAAK,SAAS,OAAO;EACtC;EACA;CACF,CAAC,GACD,SAAS,IAAI,CACf,CACF;AACF,CAAC;;AAGH,MAAM,UACH,SACA,EAAE,MAAM,MAAM,QAAQ,OAAO,cAC5B,GAAG,SAAS,KAAK,IAAI,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,KAAK,GAAG,EAAE,GAAG,KAAK,GAAG,OAAO,GAAG,MAAM,GAAG,QAAQ;;AAGtF,MAAa,YAAY,MAA6B,QACpD,OAAO,MAAM,OAAO,MAAM,GAAG,GAAG;CAC9B,cAAc,OAAO,QAAQ;EAAE,QAAQ;EAAO,UAAU;CAAE,CAAC;CAC3D,SAAS,YACP,OAAO,IAAI,UAAU,OAAO,IAAI,aAAa;EAC3C,QAAQA,MAAI,IAAI,SAAS,OAAO,GAAG,CAAC,CAAC,CAAC,KAAK,EAAE;EAC7C,UAAUA,MAAI,KAAK,UAAU,EAAE,YAAY,UAAU,OAAO,IAAI,IAAI;CACtE,EAAE;AACN,CAAC"}
|
package/dist/completion.d.mts
CHANGED
|
@@ -45,7 +45,10 @@ declare const asTagName: (title: string) => string;
|
|
|
45
45
|
declare const isTaggable: (title: string) => boolean;
|
|
46
46
|
/** The pages that complete what is typed at `position`, or none outside a link or a tag. */
|
|
47
47
|
declare const completionItems: (index: Index, text: string, position: Position, parseOptions?: ParseOptions) => CompletionItem[];
|
|
48
|
-
/**
|
|
48
|
+
/**
|
|
49
|
+
* The file of the page linked at `position`, opened at its top. None for a missing page, and
|
|
50
|
+
* for a link that leads outside the workspace (another project, a URL).
|
|
51
|
+
*/
|
|
49
52
|
declare const definitionOf: (index: Index, text: string, position: Position, parseOptions?: ParseOptions) => Option.Option<Definition>;
|
|
50
53
|
//#endregion
|
|
51
54
|
export { CompletionDetection, asTagName, completionItems, definitionOf, detectCompletion, detectCompletionInDocument, isLinkBracket, isTaggable, normalizeForMatch };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"completion.d.mts","names":[],"sources":["../src/completion.ts"],"mappings":";;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"completion.d.mts","names":[],"sources":["../src/completion.ts"],"mappings":";;;;;;;;;;;;UAoBiB;WACN;WACA;WACA;WACA;;WAEA;;;;;;;;;;cAiCE,gBAAiB;;cAgEjB,mBACX,cACA,mBACC,OAAO,OAAO;;;;;cAqCJ,6BACX,cACA,UAAU,UACV,eAAc,iBACb,OAAO,OAAO;;;;;cAaJ,oBAAqB;;cAIrB,YAAa;;cAGb,aAAc;;cAoDd,kBACX,OAAO,OACP,cACA,UAAU,UACV,eAAc,iBACb;;;;;cAiBU,eACX,OAAO,OACP,cACA,UAAU,UACV,eAAc,iBACb,OAAO,OAAO"}
|
package/dist/completion.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { linkAt } from "./link.mjs";
|
|
1
2
|
import { Array, Match, Option, pipe } from "effect";
|
|
2
3
|
import { normalizeLineEndings, parse } from "@cosense-toolbox/parser";
|
|
3
4
|
import { collect } from "@cosense-toolbox/parser/utils";
|
|
@@ -10,7 +11,7 @@ import { CompletionItemKind } from "vscode-languageserver/node";
|
|
|
10
11
|
const OTHER_NOTATIONS = [
|
|
11
12
|
/^\$/,
|
|
12
13
|
/^\//,
|
|
13
|
-
/^[!"#%&'()*+,\-./{|}<>_
|
|
14
|
+
/^[!"#%&'()*+,\-./{|}<>_~=]+(?:\s|$)/,
|
|
14
15
|
/^.+\.icon(?:\*\d+)?$/,
|
|
15
16
|
/https?:\/\//i,
|
|
16
17
|
/\.(png|jpe?g|gif|webp|svg|bmp|avif)$/i
|
|
@@ -119,8 +120,13 @@ const completionItems = (index, text, position, parseOptions = {}) => pipe(detec
|
|
|
119
120
|
onNone: () => [],
|
|
120
121
|
onSome: (detection) => Array.filterMap(index.pages, candidate(detection, position.line))
|
|
121
122
|
}));
|
|
122
|
-
/**
|
|
123
|
-
const
|
|
123
|
+
/** A page of the reader's own project: the only kind the workspace can hold a file for. */
|
|
124
|
+
const isOwnPage = (target) => target.kind === "page" && target.project === void 0;
|
|
125
|
+
/**
|
|
126
|
+
* The file of the page linked at `position`, opened at its top. None for a missing page, and
|
|
127
|
+
* for a link that leads outside the workspace (another project, a URL).
|
|
128
|
+
*/
|
|
129
|
+
const definitionOf = (index, text, position, parseOptions = {}) => pipe(linkAt(text, position, parseOptions), Option.filter(isOwnPage), Option.map(({ title }) => normalizeForMatch(title)), Option.flatMap((key) => Array.findFirst(index.pages, (page) => normalizeForMatch(page.title) === key)), Option.map(({ uri }) => {
|
|
124
130
|
const start = {
|
|
125
131
|
line: 0,
|
|
126
132
|
character: 0
|
package/dist/completion.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"completion.mjs","names":["Arr"],"sources":["../src/completion.ts"],"sourcesContent":["import { normalizeLineEndings, parse, type ParseOptions } from \"@cosense-toolbox/parser\"\nimport { collect } from \"@cosense-toolbox/parser/utils\"\nimport { Array as Arr, Match, Option, pipe } from \"effect\"\nimport {\n type CompletionItem,\n CompletionItemKind,\n type Definition,\n type Position,\n} from \"vscode-languageserver/node\"\n\nimport type { Index, Page } from \"./workspace\"\n\n/**\n * Where a completion applies, decided from the text alone.\n *\n * Cosense's own editor closes a bracket the moment it is typed, so a link is completed\n * inside a *closed* pair rather than after an opening one. Everything here is pure: the\n * candidates are somebody else's problem (see workspace.ts).\n */\nexport interface CompletionDetection {\n readonly kind: \"link\" | \"hashtag\"\n readonly query: string\n readonly replaceStart: number\n readonly replaceEnd: number\n /** `replaceStart` up to the cursor — what the client sees as already typed. */\n readonly typedText: string\n}\n\n// --- Which bracket is a link --------------------------------------------------------------\n\n/**\n * A marker run followed by whitespace, or by nothing at all: the parser waits for a body,\n * but a menu that opens between `[* ` and its text is the flicker this rule exists to stop.\n * The markers are every character Cosense accepts in a decoration, from help-jp/文字装飾記法.\n */\nconst STARTS_DECORATION = /^[!\"#%&'()*+,\\-./{|}<>_~]+(?:\\s|$)/\n\n/**\n * The notations a bracket can hold other than a page link: a formula, a project link, a\n * decoration, an icon, a URL, an image path.\n */\nconst OTHER_NOTATIONS: ReadonlyArray<RegExp> = [\n /^\\$/,\n /^\\//,\n STARTS_DECORATION,\n /^.+\\.icon(?:\\*\\d+)?$/,\n /https?:\\/\\//i,\n /\\.(png|jpe?g|gif|webp|svg|bmp|avif)$/i,\n]\n\n/**\n * True when the bracket holds a page link, or is on its way to one — the only bracket\n * completion answers in.\n *\n * The other notations (`[* 見出し]`, `[$ x^2]`, `[taro.icon]`, `[ラベル https://…]`,\n * `[a.png]`, `[/my-project/page]`) are left alone, because accepting a candidate replaces\n * the whole bracket and would take the notation with it.\n */\nexport const isLinkBracket = (inner: string): boolean =>\n inner === \"\" || !Arr.some(OTHER_NOTATIONS, (notation) => notation.test(inner))\n\n// --- Where the cursor is ------------------------------------------------------------------\n\nconst isBracket = (char: string): boolean => char === \"[\" || char === \"]\"\n\n/**\n * The `[` that opens the bracket around `cursor`: the nearest bracket to its left, if that\n * is an opening one. A `[` that is the second half of `[[` opens bold or a large image, not\n * a link.\n */\nconst openingBefore = (line: string, cursor: number): Option.Option<number> =>\n pipe(\n line.slice(0, cursor).split(\"\"),\n Arr.findLastIndex(isBracket),\n Option.filter((open) => line[open] === \"[\" && line[open - 1] !== \"[\"),\n )\n\n/** The `]` that closes it: the nearest bracket to the right of `cursor`, if a closing one. */\nconst closingAfter = (line: string, cursor: number): Option.Option<number> =>\n pipe(\n line.slice(cursor).split(\"\"),\n Arr.findFirstIndex(isBracket),\n Option.map((offset) => cursor + offset),\n Option.filter((close) => line[close] === \"]\"),\n )\n\n/** Only inside a *closed* pair, matching Cosense. */\nconst detectLink = (line: string, cursor: number): Option.Option<CompletionDetection> =>\n pipe(\n Option.all([openingBefore(line, cursor), closingAfter(line, cursor)]),\n // The whole bracket content, not just up to the cursor: Cosense treats the link text\n // as one unit, so the candidates are the same wherever the cursor sits.\n Option.map(([open, close]) => ({ open, close, inner: line.slice(open + 1, close) })),\n Option.filter(({ inner }) => isLinkBracket(inner)),\n Option.map(({ open, close, inner }) => ({\n kind: \"link\",\n query: inner,\n replaceStart: open,\n replaceEnd: close + 1,\n typedText: line.slice(open, cursor),\n })),\n )\n\n/**\n * A `#` at a tag boundary (the line's start, or after whitespace) and the tag name typed\n * after it up to the cursor, mirroring the parser's hashtag construct.\n */\nconst TAG_BEFORE_CURSOR = /(?:^|(?<=\\s))#([^\\s[\\]#]*)$/\n\nconst detectHashtag = (line: string, cursor: number): Option.Option<CompletionDetection> =>\n pipe(\n Option.fromNullable(TAG_BEFORE_CURSOR.exec(line.slice(0, cursor))),\n Option.map(({ index, 1: query = \"\" }) => ({\n kind: \"hashtag\",\n query,\n replaceStart: index,\n replaceEnd: cursor,\n typedText: line.slice(index, cursor),\n })),\n )\n\n/** Pure line/cursor -> completion trigger detection. No AST, no I/O. */\nexport const detectCompletion = (\n line: string,\n cursor: number,\n): Option.Option<CompletionDetection> =>\n pipe(\n detectLink(line, cursor),\n Option.orElse(() => detectHashtag(line, cursor)),\n )\n\n// --- In a document ------------------------------------------------------------------------\n\n/**\n * Inside code, a bracket is not notation. The cursor must be strictly between the\n * backticks (not touching them) to count as inside an inline span.\n */\nconst isInCode = (\n text: string,\n { line, character }: Position,\n parseOptions: ParseOptions,\n): boolean => {\n const page = parse(text, parseOptions)\n return (\n Arr.some(\n collect(page, \"codeBlock\"),\n ({ position }) => line >= position.start.line && line <= position.end.line,\n ) ||\n Arr.some(\n collect(page, \"inlineCode\"),\n ({ position }) =>\n line === position.start.line &&\n character > position.start.column &&\n character < position.end.column,\n )\n )\n}\n\n/**\n * `detectCompletion` for one line of a document, suppressed inside code. `parseOptions` are\n * the site's notation extensions, parsed with as the site's build parses.\n */\nexport const detectCompletionInDocument = (\n text: string,\n position: Position,\n parseOptions: ParseOptions = {},\n): Option.Option<CompletionDetection> =>\n pipe(\n Option.liftPredicate(text, (document) => !isInCode(document, position, parseOptions)),\n Option.map((document) => normalizeLineEndings(document).split(\"\\n\")[position.line] ?? \"\"),\n Option.flatMap((line) => detectCompletion(line, position.character)),\n )\n\n// --- Matching titles ----------------------------------------------------------------------\n\n/**\n * Title matching, so `[Side Kanban]` and `[side_kanban]` count as one page the way Cosense\n * links them: case-insensitive, space and `_` alike, and NFKC so width variants match too.\n */\nexport const normalizeForMatch = (s: string): string =>\n s.normalize(\"NFKC\").toLowerCase().replace(/[_ ]/g, \" \")\n\n/** The title as a tag writes it: Cosense folds a space to `_`. */\nexport const asTagName = (title: string): string => title.replace(/ /g, \"_\")\n\n/** A title that cannot be written as a tag at all is not offered as one. */\nexport const isTaggable = (title: string): boolean => !/[\\s[\\]#]/.test(asTagName(title))\n\n// --- Answers ------------------------------------------------------------------------------\n\n/** The last part of a `/`-separated location. */\nconst fileNameOf = (location: string): string =>\n Option.getOrElse(Arr.last(location.split(\"/\")), () => location)\n\n/**\n * A page as a candidate for `detection`, or None when it does not fit: a tag cannot hold a\n * space or a bracket, so a page whose title does is not offered after `#` (accepting it\n * would write something that reads as two tags).\n */\nconst candidate =\n (detection: CompletionDetection, line: number) =>\n (page: Page): Option.Option<CompletionItem> => {\n const query = normalizeForMatch(detection.query)\n const normalized = normalizeForMatch(page.title)\n const notation = Match.value(detection.kind).pipe(\n Match.when(\"hashtag\", () => `#${asTagName(page.title)}`),\n Match.when(\"link\", () => `[${page.title}]`),\n Match.exhaustive,\n )\n return pipe(\n Option.some(page),\n Option.filter(() => detection.kind === \"link\" || isTaggable(page.title)),\n Option.filter(() => query === \"\" || normalized.includes(query)),\n Option.map((): CompletionItem => ({\n label: page.title,\n kind: CompletionItemKind.File,\n // The file name alone: a whole path is cut off in the menu before the name shows.\n detail: fileNameOf(page.location),\n // The whole path, where the editor shows more room once the item is selected.\n documentation: page.location,\n // The whole notation is replaced, brackets included: the reader typed the `[`,\n // and leaving it in place would give `[[title]]`.\n textEdit: {\n range: {\n start: { line, character: detection.replaceStart },\n end: { line, character: detection.replaceEnd },\n },\n newText: notation,\n },\n // A client filters by the text from the edit's start to the cursor, `[設` here, so\n // it has to find that in the notation rather than in the bare title.\n filterText: notation,\n sortText: `${normalized} ${page.location}`,\n })),\n )\n }\n\n/** The pages that complete what is typed at `position`, or none outside a link or a tag. */\nexport const completionItems = (\n index: Index,\n text: string,\n position: Position,\n parseOptions: ParseOptions = {},\n): CompletionItem[] =>\n pipe(\n detectCompletionInDocument(text, position, parseOptions),\n Option.match({\n onNone: () => [],\n onSome: (detection) => Arr.filterMap(index.pages, candidate(detection, position.line)),\n }),\n )\n\n/** The file of the page named at `position`, opened at its top. None for a missing page. */\nexport const definitionOf = (\n index: Index,\n text: string,\n position: Position,\n parseOptions: ParseOptions = {},\n): Option.Option<Definition> =>\n pipe(\n detectCompletionInDocument(text, position, parseOptions),\n Option.map(({ query }) => normalizeForMatch(query)),\n Option.flatMap((key) =>\n Arr.findFirst(index.pages, (page) => normalizeForMatch(page.title) === key),\n ),\n Option.map(({ uri }) => {\n const start = { line: 0, character: 0 }\n return { uri, range: { start, end: start } }\n }),\n )\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"completion.mjs","names":["Arr"],"sources":["../src/completion.ts"],"sourcesContent":["import { normalizeLineEndings, parse, type ParseOptions } from \"@cosense-toolbox/parser\"\nimport { collect } from \"@cosense-toolbox/parser/utils\"\nimport { Array as Arr, Match, Option, pipe } from \"effect\"\nimport {\n type CompletionItem,\n CompletionItemKind,\n type Definition,\n type Position,\n} from \"vscode-languageserver/node\"\n\nimport { linkAt, type LinkTarget, type PageTarget } from \"./link\"\nimport type { Index, Page } from \"./workspace\"\n\n/**\n * Where a completion applies, decided from the text alone.\n *\n * Cosense's own editor closes a bracket the moment it is typed, so a link is completed\n * inside a *closed* pair rather than after an opening one. Everything here is pure: the\n * candidates are somebody else's problem (see workspace.ts).\n */\nexport interface CompletionDetection {\n readonly kind: \"link\" | \"hashtag\"\n readonly query: string\n readonly replaceStart: number\n readonly replaceEnd: number\n /** `replaceStart` up to the cursor — what the client sees as already typed. */\n readonly typedText: string\n}\n\n// --- Which bracket is a link --------------------------------------------------------------\n\n/**\n * A marker run followed by whitespace, or by nothing at all: the parser waits for a body,\n * but a menu that opens between `[* ` and its text is the flicker this rule exists to stop.\n * The markers are every character Cosense accepts in a decoration, from help-jp/文字装飾記法.\n */\nconst STARTS_DECORATION = /^[!\"#%&'()*+,\\-./{|}<>_~=]+(?:\\s|$)/\n\n/**\n * The notations a bracket can hold other than a page link: a formula, a project link, a\n * decoration, an icon, a URL, an image path.\n */\nconst OTHER_NOTATIONS: ReadonlyArray<RegExp> = [\n /^\\$/,\n /^\\//,\n STARTS_DECORATION,\n /^.+\\.icon(?:\\*\\d+)?$/,\n /https?:\\/\\//i,\n /\\.(png|jpe?g|gif|webp|svg|bmp|avif)$/i,\n]\n\n/**\n * True when the bracket holds a page link, or is on its way to one — the only bracket\n * completion answers in.\n *\n * The other notations (`[* 見出し]`, `[$ x^2]`, `[taro.icon]`, `[ラベル https://…]`,\n * `[a.png]`, `[/my-project/page]`) are left alone, because accepting a candidate replaces\n * the whole bracket and would take the notation with it.\n */\nexport const isLinkBracket = (inner: string): boolean =>\n inner === \"\" || !Arr.some(OTHER_NOTATIONS, (notation) => notation.test(inner))\n\n// --- Where the cursor is ------------------------------------------------------------------\n\nconst isBracket = (char: string): boolean => char === \"[\" || char === \"]\"\n\n/**\n * The `[` that opens the bracket around `cursor`: the nearest bracket to its left, if that\n * is an opening one. A `[` that is the second half of `[[` opens bold or a large image, not\n * a link.\n */\nconst openingBefore = (line: string, cursor: number): Option.Option<number> =>\n pipe(\n line.slice(0, cursor).split(\"\"),\n Arr.findLastIndex(isBracket),\n Option.filter((open) => line[open] === \"[\" && line[open - 1] !== \"[\"),\n )\n\n/** The `]` that closes it: the nearest bracket to the right of `cursor`, if a closing one. */\nconst closingAfter = (line: string, cursor: number): Option.Option<number> =>\n pipe(\n line.slice(cursor).split(\"\"),\n Arr.findFirstIndex(isBracket),\n Option.map((offset) => cursor + offset),\n Option.filter((close) => line[close] === \"]\"),\n )\n\n/** Only inside a *closed* pair, matching Cosense. */\nconst detectLink = (line: string, cursor: number): Option.Option<CompletionDetection> =>\n pipe(\n Option.all([openingBefore(line, cursor), closingAfter(line, cursor)]),\n // The whole bracket content, not just up to the cursor: Cosense treats the link text\n // as one unit, so the candidates are the same wherever the cursor sits.\n Option.map(([open, close]) => ({ open, close, inner: line.slice(open + 1, close) })),\n Option.filter(({ inner }) => isLinkBracket(inner)),\n Option.map(({ open, close, inner }) => ({\n kind: \"link\",\n query: inner,\n replaceStart: open,\n replaceEnd: close + 1,\n typedText: line.slice(open, cursor),\n })),\n )\n\n/**\n * A `#` at a tag boundary (the line's start, or after whitespace) and the tag name typed\n * after it up to the cursor, mirroring the parser's hashtag construct.\n */\nconst TAG_BEFORE_CURSOR = /(?:^|(?<=\\s))#([^\\s[\\]#]*)$/\n\nconst detectHashtag = (line: string, cursor: number): Option.Option<CompletionDetection> =>\n pipe(\n Option.fromNullable(TAG_BEFORE_CURSOR.exec(line.slice(0, cursor))),\n Option.map(({ index, 1: query = \"\" }) => ({\n kind: \"hashtag\",\n query,\n replaceStart: index,\n replaceEnd: cursor,\n typedText: line.slice(index, cursor),\n })),\n )\n\n/** Pure line/cursor -> completion trigger detection. No AST, no I/O. */\nexport const detectCompletion = (\n line: string,\n cursor: number,\n): Option.Option<CompletionDetection> =>\n pipe(\n detectLink(line, cursor),\n Option.orElse(() => detectHashtag(line, cursor)),\n )\n\n// --- In a document ------------------------------------------------------------------------\n\n/**\n * Inside code, a bracket is not notation. The cursor must be strictly between the\n * backticks (not touching them) to count as inside an inline span.\n */\nconst isInCode = (\n text: string,\n { line, character }: Position,\n parseOptions: ParseOptions,\n): boolean => {\n const page = parse(text, parseOptions)\n return (\n Arr.some(\n collect(page, \"codeBlock\"),\n ({ position }) => line >= position.start.line && line <= position.end.line,\n ) ||\n Arr.some(\n collect(page, \"inlineCode\"),\n ({ position }) =>\n line === position.start.line &&\n character > position.start.column &&\n character < position.end.column,\n )\n )\n}\n\n/**\n * `detectCompletion` for one line of a document, suppressed inside code. `parseOptions` are\n * the site's notation extensions, parsed with as the site's build parses.\n */\nexport const detectCompletionInDocument = (\n text: string,\n position: Position,\n parseOptions: ParseOptions = {},\n): Option.Option<CompletionDetection> =>\n pipe(\n Option.liftPredicate(text, (document) => !isInCode(document, position, parseOptions)),\n Option.map((document) => normalizeLineEndings(document).split(\"\\n\")[position.line] ?? \"\"),\n Option.flatMap((line) => detectCompletion(line, position.character)),\n )\n\n// --- Matching titles ----------------------------------------------------------------------\n\n/**\n * Title matching, so `[Side Kanban]` and `[side_kanban]` count as one page the way Cosense\n * links them: case-insensitive, space and `_` alike, and NFKC so width variants match too.\n */\nexport const normalizeForMatch = (s: string): string =>\n s.normalize(\"NFKC\").toLowerCase().replace(/[_ ]/g, \" \")\n\n/** The title as a tag writes it: Cosense folds a space to `_`. */\nexport const asTagName = (title: string): string => title.replace(/ /g, \"_\")\n\n/** A title that cannot be written as a tag at all is not offered as one. */\nexport const isTaggable = (title: string): boolean => !/[\\s[\\]#]/.test(asTagName(title))\n\n// --- Answers ------------------------------------------------------------------------------\n\n/** The last part of a `/`-separated location. */\nconst fileNameOf = (location: string): string =>\n Option.getOrElse(Arr.last(location.split(\"/\")), () => location)\n\n/**\n * A page as a candidate for `detection`, or None when it does not fit: a tag cannot hold a\n * space or a bracket, so a page whose title does is not offered after `#` (accepting it\n * would write something that reads as two tags).\n */\nconst candidate =\n (detection: CompletionDetection, line: number) =>\n (page: Page): Option.Option<CompletionItem> => {\n const query = normalizeForMatch(detection.query)\n const normalized = normalizeForMatch(page.title)\n const notation = Match.value(detection.kind).pipe(\n Match.when(\"hashtag\", () => `#${asTagName(page.title)}`),\n Match.when(\"link\", () => `[${page.title}]`),\n Match.exhaustive,\n )\n return pipe(\n Option.some(page),\n Option.filter(() => detection.kind === \"link\" || isTaggable(page.title)),\n Option.filter(() => query === \"\" || normalized.includes(query)),\n Option.map((): CompletionItem => ({\n label: page.title,\n kind: CompletionItemKind.File,\n // The file name alone: a whole path is cut off in the menu before the name shows.\n detail: fileNameOf(page.location),\n // The whole path, where the editor shows more room once the item is selected.\n documentation: page.location,\n // The whole notation is replaced, brackets included: the reader typed the `[`,\n // and leaving it in place would give `[[title]]`.\n textEdit: {\n range: {\n start: { line, character: detection.replaceStart },\n end: { line, character: detection.replaceEnd },\n },\n newText: notation,\n },\n // A client filters by the text from the edit's start to the cursor, `[設` here, so\n // it has to find that in the notation rather than in the bare title.\n filterText: notation,\n sortText: `${normalized} ${page.location}`,\n })),\n )\n }\n\n/** The pages that complete what is typed at `position`, or none outside a link or a tag. */\nexport const completionItems = (\n index: Index,\n text: string,\n position: Position,\n parseOptions: ParseOptions = {},\n): CompletionItem[] =>\n pipe(\n detectCompletionInDocument(text, position, parseOptions),\n Option.match({\n onNone: () => [],\n onSome: (detection) => Arr.filterMap(index.pages, candidate(detection, position.line)),\n }),\n )\n\n/** A page of the reader's own project: the only kind the workspace can hold a file for. */\nconst isOwnPage = (target: LinkTarget): target is PageTarget =>\n target.kind === \"page\" && target.project === undefined\n\n/**\n * The file of the page linked at `position`, opened at its top. None for a missing page, and\n * for a link that leads outside the workspace (another project, a URL).\n */\nexport const definitionOf = (\n index: Index,\n text: string,\n position: Position,\n parseOptions: ParseOptions = {},\n): Option.Option<Definition> =>\n pipe(\n linkAt(text, position, parseOptions),\n Option.filter(isOwnPage),\n Option.map(({ title }) => normalizeForMatch(title)),\n Option.flatMap((key) =>\n Arr.findFirst(index.pages, (page) => normalizeForMatch(page.title) === key),\n ),\n Option.map(({ uri }) => {\n const start = { line: 0, character: 0 }\n return { uri, range: { start, end: start } }\n }),\n )\n"],"mappings":"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"}
|
package/dist/link.d.mts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Option } from "effect";
|
|
2
|
+
import { ParseOptions } from "@cosense-toolbox/parser";
|
|
3
|
+
import { Position, Range } from "vscode-languageserver/node";
|
|
4
|
+
//#region src/link.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* A page a link leads to. `project` is there only when the link names one
|
|
7
|
+
* (`[/help-jp/page]`); without it the page is in the reader's own project. `lineId` is
|
|
8
|
+
* there only when the link names a line of the page (`[page#<line id>]`).
|
|
9
|
+
*/
|
|
10
|
+
interface PageTarget {
|
|
11
|
+
readonly kind: "page";
|
|
12
|
+
readonly project?: string;
|
|
13
|
+
/** As written: empty for `[/project]`, which leads to the project rather than a page. */
|
|
14
|
+
readonly title: string;
|
|
15
|
+
readonly lineId?: string;
|
|
16
|
+
readonly range: Range;
|
|
17
|
+
}
|
|
18
|
+
/** A URL a link leads to: an external link, or an image and where it links. */
|
|
19
|
+
interface UrlTarget {
|
|
20
|
+
readonly kind: "url";
|
|
21
|
+
readonly url: string;
|
|
22
|
+
readonly range: Range;
|
|
23
|
+
}
|
|
24
|
+
/** What a link leads to, and the range of the whole notation it is written as. */
|
|
25
|
+
type LinkTarget = PageTarget | UrlTarget;
|
|
26
|
+
/**
|
|
27
|
+
* The link at `position`, and what it leads to. None where there is no link: on plain text,
|
|
28
|
+
* on the markers of a decoration, in code.
|
|
29
|
+
*
|
|
30
|
+
* Resolving the target (to a file, a URL, a page of the web) is the caller's: it may be
|
|
31
|
+
* asynchronous, or fail, in ways only the caller knows. `parseOptions` are the site's
|
|
32
|
+
* notation extensions, so a bracket reads as the site's build reads it.
|
|
33
|
+
*/
|
|
34
|
+
declare const linkAt: (text: string, position: Position, parseOptions?: ParseOptions) => Option.Option<LinkTarget>;
|
|
35
|
+
//#endregion
|
|
36
|
+
export { LinkTarget, PageTarget, UrlTarget, linkAt };
|
|
37
|
+
//# sourceMappingURL=link.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"link.d.mts","names":[],"sources":["../src/link.ts"],"mappings":";;;;;;;;;UAYiB;WACN;WACA;;WAEA;WACA;WACA,OAAO;;;UAID;WACN;WACA;WACA,OAAO;;;KAIN,aAAa,aAAa;;;;;;;;;cA4GzB,SACX,cACA,UAAU,UACV,eAAc,iBACb,OAAO,OAAO"}
|
package/dist/link.mjs
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { n as gather, r as leaf, t as branch } from "./tree-sXzD2703.mjs";
|
|
2
|
+
import { Array, Match, Option, pipe } from "effect";
|
|
3
|
+
import { normalizeLineEndings, parse } from "@cosense-toolbox/parser";
|
|
4
|
+
//#region src/link.ts
|
|
5
|
+
/** The id Cosense gives every line, after the `#` that ends a link to it. */
|
|
6
|
+
const LINE_LINK = /^(.+)#([0-9a-f]{24})$/;
|
|
7
|
+
const ICON = /^(.+)\.icon(?:\*\d+)?$/;
|
|
8
|
+
/** `/project/name`, as an icon of another project writes its user. */
|
|
9
|
+
const OF_PROJECT = /^\/([^/]+)\/(.+)$/;
|
|
10
|
+
/** `{ [key]: value }` when there is a value, else nothing: an absent field, not `undefined`. */
|
|
11
|
+
const fieldOf = (key, value) => Option.match(value, {
|
|
12
|
+
onNone: () => ({}),
|
|
13
|
+
onSome: (present) => ({ [key]: present })
|
|
14
|
+
});
|
|
15
|
+
/** The parser counts columns in UTF-16 units, as the LSP does, so they carry over as is. */
|
|
16
|
+
const rangeOf = ({ start, end }) => ({
|
|
17
|
+
start: {
|
|
18
|
+
line: start.line,
|
|
19
|
+
character: start.column
|
|
20
|
+
},
|
|
21
|
+
end: {
|
|
22
|
+
line: end.line,
|
|
23
|
+
character: end.column
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
const pageOf = (range, project, written) => pipe(Option.fromNullable(LINE_LINK.exec(written)), Option.match({
|
|
27
|
+
onNone: () => ({
|
|
28
|
+
title: written,
|
|
29
|
+
lineId: Option.none()
|
|
30
|
+
}),
|
|
31
|
+
onSome: ([, title = "", lineId = ""]) => ({
|
|
32
|
+
title,
|
|
33
|
+
lineId: Option.some(lineId)
|
|
34
|
+
})
|
|
35
|
+
}), ({ title, lineId }) => ({
|
|
36
|
+
kind: "page",
|
|
37
|
+
...fieldOf("project", project),
|
|
38
|
+
title,
|
|
39
|
+
...fieldOf("lineId", lineId),
|
|
40
|
+
range
|
|
41
|
+
}));
|
|
42
|
+
const urlOf = (range, url) => ({
|
|
43
|
+
kind: "url",
|
|
44
|
+
url,
|
|
45
|
+
range
|
|
46
|
+
});
|
|
47
|
+
/** The page of an icon's user, who may be of another project (`/icons/taro`). */
|
|
48
|
+
const iconPageOf = (range, user) => pipe(Option.fromNullable(OF_PROJECT.exec(user)), Option.match({
|
|
49
|
+
onNone: () => pageOf(range, Option.none(), user),
|
|
50
|
+
onSome: ([, project = "", name = ""]) => pageOf(range, Option.some(project), name)
|
|
51
|
+
}));
|
|
52
|
+
/**
|
|
53
|
+
* `[[taro.icon]]`, which the parser reads as bold text (`[[x]]`) rather than as an icon.
|
|
54
|
+
* Only the double bracket counts: `[* taro.icon]` is bold text that Cosense shows as is.
|
|
55
|
+
*/
|
|
56
|
+
const largeIconOf = (lines, range, value) => pipe(Option.fromNullable(lines[range.start.line]), Option.filter((line) => line.startsWith("[[", range.start.character)), Option.flatMap(() => Option.fromNullable(ICON.exec(value))), Option.map(([, user = ""]) => iconPageOf(range, user)));
|
|
57
|
+
const targetsOf = (lines) => (node) => Match.value(node).pipe(Match.discriminators("type")({
|
|
58
|
+
internalLink: ({ target, position }) => leaf([pageOf(rangeOf(position), Option.none(), target)]),
|
|
59
|
+
hashtag: ({ value, position }) => leaf([pageOf(rangeOf(position), Option.none(), value)]),
|
|
60
|
+
projectLink: ({ project, title, position }) => leaf([pageOf(rangeOf(position), Option.some(project), title)]),
|
|
61
|
+
icon: ({ user, position }) => leaf([iconPageOf(rangeOf(position), user)]),
|
|
62
|
+
externalLink: ({ target, position }) => leaf([urlOf(rangeOf(position), target)]),
|
|
63
|
+
image: ({ src, link, position }) => leaf([urlOf(rangeOf(position), link ?? src)]),
|
|
64
|
+
decoration: ({ value, position }) => pipe(largeIconOf(lines, rangeOf(position), value), Option.match({
|
|
65
|
+
onNone: () => branch([]),
|
|
66
|
+
onSome: (page) => leaf([page])
|
|
67
|
+
}))
|
|
68
|
+
}), Match.orElse(() => branch([])));
|
|
69
|
+
/**
|
|
70
|
+
* Whether `character` is on the notation at `range`: on any of its characters, or just
|
|
71
|
+
* after the last one, where the cursor is left after typing it.
|
|
72
|
+
*/
|
|
73
|
+
const touches = (range, { line, character }) => range.start.line === line && range.start.character <= character && character <= range.end.character;
|
|
74
|
+
/**
|
|
75
|
+
* The link at `position`, and what it leads to. None where there is no link: on plain text,
|
|
76
|
+
* on the markers of a decoration, in code.
|
|
77
|
+
*
|
|
78
|
+
* Resolving the target (to a file, a URL, a page of the web) is the caller's: it may be
|
|
79
|
+
* asynchronous, or fail, in ways only the caller knows. `parseOptions` are the site's
|
|
80
|
+
* notation extensions, so a bracket reads as the site's build reads it.
|
|
81
|
+
*/
|
|
82
|
+
const linkAt = (text, position, parseOptions = {}) => {
|
|
83
|
+
const normalized = normalizeLineEndings(text);
|
|
84
|
+
const lines = normalized.split("\n");
|
|
85
|
+
const under = Array.filter(gather(parse(normalized, parseOptions), targetsOf(lines)), (target) => touches(target.range, position));
|
|
86
|
+
return pipe(Array.findFirst(under, ({ range }) => position.character < range.end.character), Option.orElse(() => Array.head(under)));
|
|
87
|
+
};
|
|
88
|
+
//#endregion
|
|
89
|
+
export { linkAt };
|
|
90
|
+
|
|
91
|
+
//# sourceMappingURL=link.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"link.mjs","names":["Arr"],"sources":["../src/link.ts"],"sourcesContent":["import type { AnyNode, ParseOptions, Position as NodePosition } from \"@cosense-toolbox/parser\"\nimport { normalizeLineEndings, parse } from \"@cosense-toolbox/parser\"\nimport { Array as Arr, Match, Option, pipe } from \"effect\"\nimport type { Position, Range } from \"vscode-languageserver/node\"\n\nimport { branch, gather, leaf, type Picked } from \"./tree\"\n\n/**\n * A page a link leads to. `project` is there only when the link names one\n * (`[/help-jp/page]`); without it the page is in the reader's own project. `lineId` is\n * there only when the link names a line of the page (`[page#<line id>]`).\n */\nexport interface PageTarget {\n readonly kind: \"page\"\n readonly project?: string\n /** As written: empty for `[/project]`, which leads to the project rather than a page. */\n readonly title: string\n readonly lineId?: string\n readonly range: Range\n}\n\n/** A URL a link leads to: an external link, or an image and where it links. */\nexport interface UrlTarget {\n readonly kind: \"url\"\n readonly url: string\n readonly range: Range\n}\n\n/** What a link leads to, and the range of the whole notation it is written as. */\nexport type LinkTarget = PageTarget | UrlTarget\n\n/** The id Cosense gives every line, after the `#` that ends a link to it. */\nconst LINE_LINK = /^(.+)#([0-9a-f]{24})$/\n\nconst ICON = /^(.+)\\.icon(?:\\*\\d+)?$/\n\n/** `/project/name`, as an icon of another project writes its user. */\nconst OF_PROJECT = /^\\/([^/]+)\\/(.+)$/\n\n/** `{ [key]: value }` when there is a value, else nothing: an absent field, not `undefined`. */\nconst fieldOf = <K extends string, A>(key: K, value: Option.Option<A>) =>\n Option.match(value, {\n onNone: () => ({}),\n onSome: (present) => ({ [key]: present }) as { readonly [P in K]: A },\n })\n\n/** The parser counts columns in UTF-16 units, as the LSP does, so they carry over as is. */\nconst rangeOf = ({ start, end }: NodePosition): Range => ({\n start: { line: start.line, character: start.column },\n end: { line: end.line, character: end.column },\n})\n\nconst pageOf = (range: Range, project: Option.Option<string>, written: string): PageTarget =>\n pipe(\n Option.fromNullable(LINE_LINK.exec(written)),\n Option.match({\n onNone: () => ({ title: written, lineId: Option.none<string>() }),\n onSome: ([, title = \"\", lineId = \"\"]) => ({ title, lineId: Option.some(lineId) }),\n }),\n ({ title, lineId }) => ({\n kind: \"page\",\n ...fieldOf(\"project\", project),\n title,\n ...fieldOf(\"lineId\", lineId),\n range,\n }),\n )\n\nconst urlOf = (range: Range, url: string): UrlTarget => ({ kind: \"url\", url, range })\n\n/** The page of an icon's user, who may be of another project (`/icons/taro`). */\nconst iconPageOf = (range: Range, user: string): PageTarget =>\n pipe(\n Option.fromNullable(OF_PROJECT.exec(user)),\n Option.match({\n onNone: () => pageOf(range, Option.none(), user),\n onSome: ([, project = \"\", name = \"\"]) => pageOf(range, Option.some(project), name),\n }),\n )\n\n/**\n * `[[taro.icon]]`, which the parser reads as bold text (`[[x]]`) rather than as an icon.\n * Only the double bracket counts: `[* taro.icon]` is bold text that Cosense shows as is.\n */\nconst largeIconOf = (\n lines: ReadonlyArray<string>,\n range: Range,\n value: string,\n): Option.Option<PageTarget> =>\n pipe(\n Option.fromNullable(lines[range.start.line]),\n Option.filter((line) => line.startsWith(\"[[\", range.start.character)),\n Option.flatMap(() => Option.fromNullable(ICON.exec(value))),\n Option.map(([, user = \"\"]) => iconPageOf(range, user)),\n )\n\nconst targetsOf =\n (lines: ReadonlyArray<string>) =>\n (node: AnyNode): Picked<LinkTarget> =>\n Match.value(node).pipe(\n Match.discriminators(\"type\")({\n internalLink: ({ target, position }) =>\n leaf([pageOf(rangeOf(position), Option.none(), target)]),\n hashtag: ({ value, position }) => leaf([pageOf(rangeOf(position), Option.none(), value)]),\n projectLink: ({ project, title, position }) =>\n leaf([pageOf(rangeOf(position), Option.some(project), title)]),\n icon: ({ user, position }) => leaf([iconPageOf(rangeOf(position), user)]),\n externalLink: ({ target, position }) => leaf([urlOf(rangeOf(position), target)]),\n // Where the image leads, when it leads anywhere, is what following it opens.\n image: ({ src, link, position }) => leaf([urlOf(rangeOf(position), link ?? src)]),\n // A decoration can wrap links (`[! [page]です]`), so it is read into.\n decoration: ({ value, position }) =>\n pipe(\n largeIconOf(lines, rangeOf(position), value),\n Option.match({ onNone: () => branch([]), onSome: (page) => leaf([page]) }),\n ),\n }),\n Match.orElse(() => branch([])),\n )\n\n/**\n * Whether `character` is on the notation at `range`: on any of its characters, or just\n * after the last one, where the cursor is left after typing it.\n */\nconst touches = (range: Range, { line, character }: Position): boolean =>\n range.start.line === line &&\n range.start.character <= character &&\n character <= range.end.character\n\n/**\n * The link at `position`, and what it leads to. None where there is no link: on plain text,\n * on the markers of a decoration, in code.\n *\n * Resolving the target (to a file, a URL, a page of the web) is the caller's: it may be\n * asynchronous, or fail, in ways only the caller knows. `parseOptions` are the site's\n * notation extensions, so a bracket reads as the site's build reads it.\n */\nexport const linkAt = (\n text: string,\n position: Position,\n parseOptions: ParseOptions = {},\n): Option.Option<LinkTarget> => {\n const normalized = normalizeLineEndings(text)\n const lines = normalized.split(\"\\n\")\n const under = Arr.filter(gather(parse(normalized, parseOptions), targetsOf(lines)), (target) =>\n touches(target.range, position),\n )\n // Two notations can touch (`[a][b]`): the one that starts under the cursor is the one\n // it is on, and the one it is just after is only a fallback.\n return pipe(\n Arr.findFirst(under, ({ range }) => position.character < range.end.character),\n Option.orElse(() => Arr.head(under)),\n )\n}\n"],"mappings":"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"}
|
package/dist/main.mjs
CHANGED
|
@@ -13,7 +13,7 @@ const check = pipe(Effect.promise(() => import("./check.mjs")), Effect.flatMap((
|
|
|
13
13
|
(exitCode === 2 ? process.stderr : process.stdout).write(output);
|
|
14
14
|
process.exitCode = exitCode;
|
|
15
15
|
})));
|
|
16
|
-
const serve = Effect.promise(() => import("./server-
|
|
16
|
+
const serve = Effect.promise(() => import("./server-B7AZxbRw.mjs"));
|
|
17
17
|
Effect.runFork(Match.value(command).pipe(Match.when("check", () => Effect.asVoid(check)), Match.orElse(() => Effect.asVoid(serve))));
|
|
18
18
|
//#endregion
|
|
19
19
|
export {};
|
|
@@ -1,9 +1,40 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { LEGEND, computeTokens, encodeTokens } from "./tokens.mjs";
|
|
2
2
|
import { completionItems, definitionOf } from "./completion.mjs";
|
|
3
|
-
import { a as rootsOf,
|
|
4
|
-
import { Array, Effect, Option, Ref, pipe } from "effect";
|
|
3
|
+
import { a as rootsOf, o as severityOf, r as readIndex, s as unresolvedLinkDiagnostics, t as emptyIndex } from "./workspace-A-W0PU0g.mjs";
|
|
4
|
+
import { Array, Effect, Option, Record, Ref, Schema, pipe } from "effect";
|
|
5
5
|
import { ProposedFeatures, TextDocumentSyncKind, TextDocuments, createConnection } from "vscode-languageserver/node";
|
|
6
6
|
import { TextDocument } from "vscode-languageserver-textdocument";
|
|
7
|
+
//#region src/settings.ts
|
|
8
|
+
const defaultSettings = {
|
|
9
|
+
sources: [],
|
|
10
|
+
unresolvedLinks: "warning",
|
|
11
|
+
frontmatter: true
|
|
12
|
+
};
|
|
13
|
+
/** `schema`'s reading of `value`, or `fallback` when `value` is not of that shape. */
|
|
14
|
+
const decodeOr = (schema, fallback) => (value) => Option.getOrElse(Schema.decodeUnknownOption(schema)(value), () => fallback);
|
|
15
|
+
/** The options as an object, which the editor may send in any shape or not at all. */
|
|
16
|
+
const OptionsObject = Schema.Record({
|
|
17
|
+
key: Schema.String,
|
|
18
|
+
value: Schema.Unknown
|
|
19
|
+
});
|
|
20
|
+
/**
|
|
21
|
+
* The non-empty strings of a list. Anything else in it is dropped rather than failing the
|
|
22
|
+
* whole list, and anything but a list counts as none.
|
|
23
|
+
*/
|
|
24
|
+
const stringsOf = (value) => Array.filter(decodeOr(Schema.Array(Schema.Unknown), [])(value), Schema.is(Schema.NonEmptyString));
|
|
25
|
+
/**
|
|
26
|
+
* Each setting read on its own, so one of the wrong shape falls back to its default
|
|
27
|
+
* without taking the others with it.
|
|
28
|
+
*/
|
|
29
|
+
const settingsOf = (options) => {
|
|
30
|
+
const field = (key) => pipe(Schema.decodeUnknownOption(OptionsObject)(options), Option.flatMap(Record.get(key)), Option.getOrUndefined);
|
|
31
|
+
return {
|
|
32
|
+
sources: stringsOf(field("sources")),
|
|
33
|
+
unresolvedLinks: severityOf(field("unresolvedLinks")),
|
|
34
|
+
frontmatter: decodeOr(Schema.Boolean, defaultSettings.frontmatter)(field("frontmatter"))
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
//#endregion
|
|
7
38
|
//#region src/server.ts
|
|
8
39
|
const connection = createConnection(ProposedFeatures.all);
|
|
9
40
|
const documents = new TextDocuments(TextDocument);
|
|
@@ -17,11 +48,8 @@ const readsComponents = (document) => Array.contains(CSNX_LANGUAGE_IDS, document
|
|
|
17
48
|
/** What the reader set in the editor's initialization options (see settings.ts). */
|
|
18
49
|
const settings = Ref.unsafeMake(defaultSettings);
|
|
19
50
|
const currentSettings = () => Effect.runSync(Ref.get(settings));
|
|
20
|
-
/** How to parse, so notation reads as the site's build reads it. */
|
|
21
|
-
const currentParseOptions = () => parseOptionsOf(currentSettings());
|
|
22
51
|
const semanticTokensOf = (document) => ({ data: encodeTokens(computeTokens(document.getText(), {
|
|
23
52
|
components: readsComponents(document),
|
|
24
|
-
parseOptions: currentParseOptions(),
|
|
25
53
|
frontmatter: currentSettings().frontmatter
|
|
26
54
|
})) });
|
|
27
55
|
/**
|
|
@@ -49,7 +77,6 @@ const publishDiagnostics = (document) => pipe(Effect.all([Ref.get(index), Ref.ge
|
|
|
49
77
|
diagnostics: unresolvedLinkDiagnostics(pages, document.getText(), {
|
|
50
78
|
severity: set.unresolvedLinks,
|
|
51
79
|
components: readsComponents(document),
|
|
52
|
-
parseOptions: parseOptionsOf(set),
|
|
53
80
|
frontmatter: set.frontmatter
|
|
54
81
|
})
|
|
55
82
|
}))
|
|
@@ -89,12 +116,12 @@ documents.onDidClose(({ document }) => {
|
|
|
89
116
|
diagnostics: []
|
|
90
117
|
})));
|
|
91
118
|
});
|
|
92
|
-
connection.onCompletion(({ textDocument: { uri }, position }) => withDocument(uri, (document) => completionItems(currentIndex(), document.getText(), position
|
|
93
|
-
connection.onDefinition(({ textDocument: { uri }, position }) => withDocument(uri, (document) => Option.getOrNull(definitionOf(currentIndex(), document.getText(), position
|
|
119
|
+
connection.onCompletion(({ textDocument: { uri }, position }) => withDocument(uri, (document) => completionItems(currentIndex(), document.getText(), position), []));
|
|
120
|
+
connection.onDefinition(({ textDocument: { uri }, position }) => withDocument(uri, (document) => Option.getOrNull(definitionOf(currentIndex(), document.getText(), position)), null));
|
|
94
121
|
connection.languages.semanticTokens.on(({ textDocument: { uri } }) => withDocument(uri, semanticTokensOf, { data: [] }));
|
|
95
122
|
documents.listen(connection);
|
|
96
123
|
connection.listen();
|
|
97
124
|
//#endregion
|
|
98
125
|
export {};
|
|
99
126
|
|
|
100
|
-
//# sourceMappingURL=server-
|
|
127
|
+
//# sourceMappingURL=server-B7AZxbRw.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server-B7AZxbRw.mjs","names":["Arr","Rec","Arr"],"sources":["../src/settings.ts","../src/server.ts"],"sourcesContent":["import { Array as Arr, Option, pipe, Record as Rec, Schema } from \"effect\"\n\nimport { severityOf, type UnresolvedSeverity } from \"./diagnostics\"\n\n/**\n * What the reader can set, through the editor's initialization options:\n *\n * ```json\n * { \"sources\": [\"src\"], \"unresolvedLinks\": \"warning\", \"frontmatter\": true }\n * ```\n *\n * Which markers open a decoration is not among them: that is Cosense's syntax, and the parser\n * reads every one of them (`[! 注意]` included) as Cosense Web does.\n */\nexport interface Settings {\n /**\n * Where the pages are, relative to each workspace folder. Empty reads the whole folder.\n * A site keeps its pages in a few directories, and the rest of the repository is noise.\n */\n readonly sources: ReadonlyArray<string>\n /** How loudly a link to a missing page is reported. */\n readonly unresolvedLinks: UnresolvedSeverity\n /**\n * Whether a `---` fence on a page's first line opens YAML to skip. Off for Cosense pages,\n * which have no frontmatter: a page titled `---` would otherwise lose its first lines.\n */\n readonly frontmatter: boolean\n}\n\nexport const defaultSettings: Settings = {\n sources: [],\n unresolvedLinks: \"warning\",\n frontmatter: true,\n}\n\n/** `schema`'s reading of `value`, or `fallback` when `value` is not of that shape. */\nconst decodeOr =\n <A, I>(schema: Schema.Schema<A, I>, fallback: A) =>\n (value: unknown): A =>\n Option.getOrElse(Schema.decodeUnknownOption(schema)(value), () => fallback)\n\n/** The options as an object, which the editor may send in any shape or not at all. */\nconst OptionsObject = Schema.Record({ key: Schema.String, value: Schema.Unknown })\n\n/**\n * The non-empty strings of a list. Anything else in it is dropped rather than failing the\n * whole list, and anything but a list counts as none.\n */\nconst stringsOf = (value: unknown): ReadonlyArray<string> =>\n Arr.filter(decodeOr(Schema.Array(Schema.Unknown), [])(value), Schema.is(Schema.NonEmptyString))\n\n/**\n * Each setting read on its own, so one of the wrong shape falls back to its default\n * without taking the others with it.\n */\nexport const settingsOf = (options: unknown): Settings => {\n const field = (key: keyof Settings): unknown =>\n pipe(\n Schema.decodeUnknownOption(OptionsObject)(options),\n Option.flatMap(Rec.get(key)),\n Option.getOrUndefined,\n )\n return {\n sources: stringsOf(field(\"sources\")),\n unresolvedLinks: severityOf(field(\"unresolvedLinks\")),\n frontmatter: decodeOr(Schema.Boolean, defaultSettings.frontmatter)(field(\"frontmatter\")),\n }\n}\n","import { Array as Arr, Effect, Option, pipe, Ref } from \"effect\"\nimport {\n type CompletionItem,\n createConnection,\n type Definition,\n type InitializeResult,\n ProposedFeatures,\n type SemanticTokens,\n TextDocuments,\n TextDocumentSyncKind,\n} from \"vscode-languageserver/node\"\nimport { TextDocument } from \"vscode-languageserver-textdocument\"\n\nimport { completionItems, definitionOf } from \"./completion\"\nimport { unresolvedLinkDiagnostics } from \"./diagnostics\"\nimport { defaultSettings, settingsOf } from \"./settings\"\nimport { computeTokens, encodeTokens, LEGEND } from \"./tokens\"\nimport { emptyIndex, type Index, readIndex, rootsOf } from \"./workspace\"\n\nconst connection = createConnection(ProposedFeatures.all)\nconst documents = new TextDocuments(TextDocument)\n\n/** The language ids editors give `.csnx`. Not every editor names the language the same way. */\nconst CSNX_LANGUAGE_IDS: ReadonlyArray<string> = [\"csnx\", \"cosense-x\"]\n\n/**\n * `.csnx` is `.csn` plus component lines, so the format is decided per document. The\n * language id the editor sends is trusted first; a file:// URI's suffix is the fallback.\n */\nconst readsComponents = (document: TextDocument): boolean =>\n Arr.contains(CSNX_LANGUAGE_IDS, document.languageId) || document.uri.endsWith(\".csnx\")\n\n// --- Settings -----------------------------------------------------------------------------\n\n/** What the reader set in the editor's initialization options (see settings.ts). */\nconst settings = Ref.unsafeMake(defaultSettings)\n\nconst currentSettings = () => Effect.runSync(Ref.get(settings))\n\nconst semanticTokensOf = (document: TextDocument): SemanticTokens => ({\n data: encodeTokens(\n computeTokens(document.getText(), {\n components: readsComponents(document),\n frontmatter: currentSettings().frontmatter,\n }),\n ),\n})\n\n/**\n * `answer` for the open document at `uri`, or `fallback` when the editor asks about one it\n * has not synced yet.\n */\nconst withDocument = <A>(uri: string, answer: (document: TextDocument) => A, fallback: A): A =>\n pipe(\n Option.fromNullable(documents.get(uri)),\n Option.map(answer),\n Option.getOrElse(() => fallback),\n )\n\n// --- The workspace index --------------------------------------------------------------------\n\n/**\n * Every page the workspace holds. Read once at initialize and again when a page is saved,\n * because a title only changes by someone writing it, and re-reading on every keystroke\n * would walk the whole tree while the reader is typing.\n */\nconst index = Ref.unsafeMake<Option.Option<Index>>(Option.none())\nconst roots = Ref.unsafeMake<ReadonlyArray<string>>([])\n\n/** The index as it stands; empty until it has been read once. */\nconst currentIndex = (): Index => Option.getOrElse(Effect.runSync(Ref.get(index)), () => emptyIndex)\n\n// --- Diagnostics --------------------------------------------------------------------------\n\n/**\n * Sends `document`'s links to missing pages. Nothing until the index has been read once:\n * against an empty index every link would be flagged, only to be cleared a moment later.\n */\nconst publishDiagnostics = (document: TextDocument): Effect.Effect<void> =>\n pipe(\n Effect.all([Ref.get(index), Ref.get(settings)]),\n Effect.flatMap(([read, set]) =>\n Option.match(read, {\n onNone: () => Effect.void,\n onSome: (pages) =>\n Effect.promise(() =>\n connection.sendDiagnostics({\n uri: document.uri,\n diagnostics: unresolvedLinkDiagnostics(pages, document.getText(), {\n severity: set.unresolvedLinks,\n components: readsComponents(document),\n frontmatter: set.frontmatter,\n }),\n }),\n ),\n }),\n ),\n )\n\n/**\n * Reads the index again in the background; requests meanwhile answer from the last one.\n * A page may have appeared or gone, so every open document is checked again after.\n */\nconst refreshIndex = (): void => {\n pipe(\n Effect.all([Ref.get(roots), Ref.get(settings)]),\n Effect.flatMap(([at, set]) => readIndex(at, { frontmatter: set.frontmatter })),\n Effect.flatMap((next) => Ref.set(index, Option.some(next))),\n Effect.flatMap(() => Effect.forEach(documents.all(), publishDiagnostics, { discard: true })),\n Effect.runFork,\n )\n}\n\n// --- Handlers -----------------------------------------------------------------------------\n\nconnection.onInitialize(({ workspaceFolders, initializationOptions }): InitializeResult => {\n const read = settingsOf(initializationOptions)\n Effect.runSync(Ref.set(settings, read))\n Effect.runSync(Ref.set(roots, rootsOf(workspaceFolders, read.sources)))\n refreshIndex()\n return {\n capabilities: {\n // Incremental: a page is one file the reader types into, and resending all of it on\n // every keystroke is what makes a large page feel slow.\n textDocumentSync: TextDocumentSyncKind.Incremental,\n semanticTokensProvider: {\n legend: { tokenTypes: [...LEGEND], tokenModifiers: [] },\n full: true,\n },\n // `[` and `#` open a link and a tag. The editor asks again on every character after\n // one, so nothing else has to be declared for typing to keep the menu up to date.\n completionProvider: { triggerCharacters: [\"[\", \"#\"] },\n definitionProvider: true,\n },\n }\n})\n\n// A title only changes when a page is written, so the index is rebuilt then rather than\n// on a timer or on every request.\ndocuments.onDidSave(refreshIndex)\n\n// Opening a document counts as a change, so this covers both.\ndocuments.onDidChangeContent(({ document }) => {\n Effect.runFork(publishDiagnostics(document))\n})\n\n// A closed document's diagnostics would otherwise linger in the editor's problem list.\ndocuments.onDidClose(({ document }) => {\n Effect.runFork(\n Effect.promise(() => connection.sendDiagnostics({ uri: document.uri, diagnostics: [] })),\n )\n})\n\nconnection.onCompletion(({ textDocument: { uri }, position }): CompletionItem[] =>\n withDocument(\n uri,\n (document) => completionItems(currentIndex(), document.getText(), position),\n [],\n ),\n)\n\nconnection.onDefinition(({ textDocument: { uri }, position }): Definition | null =>\n withDocument(\n uri,\n (document) => Option.getOrNull(definitionOf(currentIndex(), document.getText(), position)),\n null,\n ),\n)\n\nconnection.languages.semanticTokens.on(({ textDocument: { uri } }): SemanticTokens =>\n // Asked before the document was synced: nothing to colour yet.\n withDocument(uri, semanticTokensOf, { data: [] }),\n)\n\ndocuments.listen(connection)\nconnection.listen()\n"],"mappings":"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"}
|