@cosense-toolbox/lsp 0.1.0-beta.3 → 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 CHANGED
@@ -54,4 +54,13 @@ csn-lsp check src/content src/pages
54
54
  - `notations` で装飾の記号に名前を付け、その名前のトークンとして送れる (`[! 注意]` の `!` に `warning` など)。
55
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.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { i as readPageFiles, n as indexOf, o as severityOf, s as unresolvedLinkDiagnostics } from "./workspace-XJqmAxae.mjs";
1
+ import { i as readPageFiles, n as indexOf, o as severityOf, s as unresolvedLinkDiagnostics } from "./workspace-A-W0PU0g.mjs";
2
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";
@@ -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
- /** The file of the page named at `position`, opened at its top. None for a missing page. */
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":";;;;;;;;;;;;UAmBiB;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;;cAUU,eACX,OAAO,OACP,cACA,UAAU,UACV,eAAc,iBACb,OAAO,OAAO"}
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"}
@@ -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";
@@ -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
- /** The file of the page named at `position`, opened at its top. None for a missing page. */
123
- const definitionOf = (index, text, position, parseOptions = {}) => pipe(detectCompletionInDocument(text, position, parseOptions), Option.map(({ query }) => normalizeForMatch(query)), Option.flatMap((key) => Array.findFirst(index.pages, (page) => normalizeForMatch(page.title) === key)), Option.map(({ uri }) => {
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
@@ -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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"}
@@ -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-DWgrivkG.mjs"));
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,6 +1,6 @@
1
- import { a as encodeTokens, i as computeTokens, n as LEGEND } from "./tokens-pb6vW18i.mjs";
1
+ import { LEGEND, computeTokens, encodeTokens } from "./tokens.mjs";
2
2
  import { completionItems, definitionOf } from "./completion.mjs";
3
- import { a as rootsOf, o as severityOf, r as readIndex, s as unresolvedLinkDiagnostics, t as emptyIndex } from "./workspace-XJqmAxae.mjs";
3
+ import { a as rootsOf, o as severityOf, r as readIndex, s as unresolvedLinkDiagnostics, t as emptyIndex } from "./workspace-A-W0PU0g.mjs";
4
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";
@@ -124,4 +124,4 @@ connection.listen();
124
124
  //#endregion
125
125
  export {};
126
126
 
127
- //# sourceMappingURL=server-DWgrivkG.mjs.map
127
+ //# sourceMappingURL=server-B7AZxbRw.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"server-DWgrivkG.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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"}
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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"}
package/dist/tokens.d.mts CHANGED
@@ -46,6 +46,9 @@ declare const LEGEND: readonly ["namespace", "type", "function", "variable", "pr
46
46
  /**
47
47
  * LEGEND with the names of `notations` after it, each once: the legend for a client that
48
48
  * accepts token types of the caller's own naming. The LSP's types keep their places.
49
+ *
50
+ * A client that colours by this package's own names instead passes those to `encodeTokens`
51
+ * (`[...TOKEN_TYPES, ...names]`): a token whose own name is in the legend is sent as that.
49
52
  */
50
53
  declare const legendOf: (notations: ReadonlyArray<Notation>) => ReadonlyArray<string>;
51
54
  declare const COMPONENT_LINE: RegExp;
@@ -73,7 +76,8 @@ interface ComputeTokensOptions {
73
76
  declare const computeTokens: (text: string, options?: ComputeTokensOptions) => RawToken[];
74
77
  /**
75
78
  * LSP relative encoding: [deltaLine, deltaStartChar, length, tokenType, tokenModifiers]*.
76
- * `legend` is the one sent at initialize: LEGEND, or `legendOf(notations)`.
79
+ * `legend` is the one sent at initialize: LEGEND, `legendOf(notations)`, or a legend of the
80
+ * client's own naming such as `[...TOKEN_TYPES, ...names]`.
77
81
  */
78
82
  declare const encodeTokens: (tokens: ReadonlyArray<RawToken>, legend?: ReadonlyArray<string>) => number[];
79
83
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"tokens.d.mts","names":[],"sources":["../src/tokens.ts"],"mappings":";;;;;;;;;;;cAqBa;KAgCD,oBAAoB;;KAG3B,mBAAmB,QAAQ;UAEtB;WACC;WACA;WACA;;KAGC,YACP;WAAkB,MAAM;MACxB;WAAkB;WAA2B;;;;;;;;;;UAUjC;WACN;WACA;;;;;;;;cASE;;;;;cAqDA,WAAY,WAAW,cAAc,cAAY;cAkEjD,gBAAc;;cA2Gd,iBAAkB,OAAO,0BAAwB,OAAO;;;;;cAUxD,UACX,OAAO,uBACP,yBACC,OAAO;UAgIO;;WAEN;;WAEA,eAAe;;WAEf,YAAY,cAAc;;;;;WAK1B;;;cAIE,gBAAiB,cAAc,UAAS,yBAA4B;;;;;cA+CpE,eACX,QAAQ,cAAc,WACtB,SAAQ"}
1
+ {"version":3,"file":"tokens.d.mts","names":[],"sources":["../src/tokens.ts"],"mappings":";;;;;;;;;;;cAqBa;KAgCD,oBAAoB;;KAG3B,mBAAmB,QAAQ;UAEtB;WACC;WACA;WACA;;KAGC,YACP;WAAkB,MAAM;MACxB;WAAkB;WAA2B;;;;;;;;;;UAUjC;WACN;WACA;;;;;;;;cASE;;;;;;;;cAwDA,WAAY,WAAW,cAAc,cAAY;cAkEjD,gBAAc;;cA2Gd,iBAAkB,OAAO,0BAAwB,OAAO;;;;;cAUxD,UACX,OAAO,uBACP,yBACC,OAAO;UAgIO;;WAEN;;WAEA,eAAe;;WAEf,YAAY,cAAc;;;;;WAK1B;;;cAIE,gBAAiB,cAAc,UAAS,yBAA4B;;;;;;cAqDpE,eACX,QAAQ,cAAc,WACtB,SAAQ"}