From 012b1eb4c3f95eaa1547ddfe59a66d0b587f7228 Mon Sep 17 00:00:00 2001 From: Jeremy Valentine <38669521+valentine195@users.noreply.github.com> Date: Wed, 16 Aug 2023 14:29:59 -0400 Subject: [PATCH] feat: Adds Editor Suggestor for encounter blocks (close #185) --- package-lock.json | 38 +++- package.json | 2 +- src/encounter/editor-suggestor/index.ts | 236 ++++++++++++++++++++++++ src/main.ts | 2 + 4 files changed, 268 insertions(+), 10 deletions(-) create mode 100644 src/encounter/editor-suggestor/index.ts diff --git a/package-lock.json b/package-lock.json index 7485c6c2..5e719bdb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,7 +24,7 @@ "esbuild-svelte": "^0.6.0", "fast-copy": "^3.0.1", "jest": "^29.5.0", - "obsidian": "^1.2.5", + "obsidian": "^1.4.0", "obsidian-overload": "^1.41.0", "standard-version": "^9.3.2", "svelte": "^3.49.0", @@ -6174,12 +6174,12 @@ } }, "node_modules/obsidian": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/obsidian/-/obsidian-1.2.5.tgz", - "integrity": "sha512-RKN4W3PaHsoWwlNRg1SV+iJssQ5vnQYzsCSfmFAUHvA8Q8nzk4pW3HGWXSvor3ZM532KECljG86lEx02OvBwpA==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/obsidian/-/obsidian-1.4.0.tgz", + "integrity": "sha512-fsZMPlxgflGSBSP6P4BjQi5+0MqZl3h6FEDEZ3CNnweNdDw0doyqN3FMO/PGWfuxPT77WicVwUxekuI3e6eCGg==", "dev": true, "dependencies": { - "@types/codemirror": "0.0.108", + "@types/codemirror": "5.60.8", "moment": "2.29.4" }, "peerDependencies": { @@ -6298,6 +6298,15 @@ "obsidian-leaflet": "^5.6.0" } }, + "node_modules/obsidian/node_modules/@types/codemirror": { + "version": "5.60.8", + "resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-5.60.8.tgz", + "integrity": "sha512-VjFgDF/eB+Aklcy15TtOTLQeMjTo07k7KAjql8OK5Dirr7a6sJY4T1uVBDuTVG9VEmn1uUsohOpYnVfgC6/jyw==", + "dev": true, + "dependencies": { + "@types/tern": "*" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -12875,13 +12884,24 @@ } }, "obsidian": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/obsidian/-/obsidian-1.2.5.tgz", - "integrity": "sha512-RKN4W3PaHsoWwlNRg1SV+iJssQ5vnQYzsCSfmFAUHvA8Q8nzk4pW3HGWXSvor3ZM532KECljG86lEx02OvBwpA==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/obsidian/-/obsidian-1.4.0.tgz", + "integrity": "sha512-fsZMPlxgflGSBSP6P4BjQi5+0MqZl3h6FEDEZ3CNnweNdDw0doyqN3FMO/PGWfuxPT77WicVwUxekuI3e6eCGg==", "dev": true, "requires": { - "@types/codemirror": "0.0.108", + "@types/codemirror": "5.60.8", "moment": "2.29.4" + }, + "dependencies": { + "@types/codemirror": { + "version": "5.60.8", + "resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-5.60.8.tgz", + "integrity": "sha512-VjFgDF/eB+Aklcy15TtOTLQeMjTo07k7KAjql8OK5Dirr7a6sJY4T1uVBDuTVG9VEmn1uUsohOpYnVfgC6/jyw==", + "dev": true, + "requires": { + "@types/tern": "*" + } + } } }, "obsidian-calendar-ui": { diff --git a/package.json b/package.json index d229cd6a..4b85f8b4 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "esbuild-svelte": "^0.6.0", "fast-copy": "^3.0.1", "jest": "^29.5.0", - "obsidian": "^1.2.5", + "obsidian": "^1.4.0", "obsidian-overload": "^1.41.0", "standard-version": "^9.3.2", "svelte": "^3.49.0", diff --git a/src/encounter/editor-suggestor/index.ts b/src/encounter/editor-suggestor/index.ts new file mode 100644 index 00000000..bbe89803 --- /dev/null +++ b/src/encounter/editor-suggestor/index.ts @@ -0,0 +1,236 @@ +import { + Editor, + EditorPosition, + EditorSuggest, + EditorSuggestContext, + EditorSuggestTriggerInfo, + TFile, + parseYaml +} from "obsidian"; +import type InitiativeTracker from "../../main"; + +enum SuggestContext { + Players, + Creatures, + Party, + RollHP, + Name, + None +} + +interface ParsedEncounter { + name?: string; + players?: string[]; + party?: string; + rollHP?: boolean; + creatures?: Array<{ [key: number]: string } | string>; +} + +export class EncounterSuggester extends EditorSuggest { + _context: SuggestContext = SuggestContext.None; + _encounter: ParsedEncounter = {}; + constructor(public plugin: InitiativeTracker) { + super(plugin.app); + } + getSuggestions(ctx: EditorSuggestContext) { + let suggestions: string[] = []; + + switch (this._context) { + case SuggestContext.Name: { + suggestions = []; + break; + } + case SuggestContext.Players: + suggestions = [...this.plugin.players.keys()].filter( + (p) => !this._encounter.players?.includes(p) + ); + break; + case SuggestContext.Creatures: + suggestions = this.plugin.bestiary?.map((b) => b.name); + break; + case SuggestContext.Party: + suggestions = this.plugin.data.parties?.map((p) => p.name); + break; + case SuggestContext.RollHP: + suggestions = ["true", "false"]; + break; + case SuggestContext.None: + suggestions = [ + "players", + "creatures", + "party", + "rollHP", + "name" + ].filter((k) => !(k in this._encounter)); + break; + } + return suggestions.filter( + (s) => + !ctx.query.length || + s.toLowerCase().contains(ctx.query.toLowerCase()) + ); + } + renderSuggestion(text: string, el: HTMLElement) { + el.createSpan({ text }); + } + selectSuggestion(value: string, evt: MouseEvent | KeyboardEvent): void { + if (!this.context) return; + switch (this._context) { + case SuggestContext.None: { + value = `${value}:`; + if (/^(players|creatures):/.test(value)) { + value = `${value}\n - `; + } else { + value = `${value} `; + } + break; + } + case SuggestContext.Creatures: + case SuggestContext.Players: { + const spaces = this.context.editor + .getLine(this.context.start.line) + .search(/\S/); + value = `${value}\n${" ".repeat(spaces)}- `; + break; + } + case SuggestContext.Party: + case SuggestContext.RollHP: { + const endsWithSpace = /\s$/.test( + this.context.editor.getLine(this.context.start.line) + ); + value = `${endsWithSpace ? "" : " "}${value.trim()}`; + break; + } + case SuggestContext.Name: + break; + } + + this.context.editor.replaceRange( + `${value}`, + this.context.start, + { + ...this.context.end, + ch: this.context.start.ch + this.context.query.length + }, + "initiative-tracker" + ); + + this.context.editor.setCursor( + this.context.start.line, + this.context.start.ch + value.length + ); + + this.close(); + } + onTrigger( + cursor: EditorPosition, + editor: Editor, + file: TFile + ): EditorSuggestTriggerInfo | null { + this._context = SuggestContext.None; + const range = editor.getRange({ line: 0, ch: 0 }, cursor); + + if (range.indexOf("```encounter\n") === -1) return null; + + const split = range.split("\n"); + + let inEncounter = false, + start: number; + for (let i = split.length - 1; i >= 0; i--) { + let line = split[i]; + if (/^```$/.test(line)) return null; + if (/^```encounter/.test(line)) { + inEncounter = true; + start = i; + break; + } + } + if (!inEncounter) return null; + + /** get all the keys in the encounter block */ + try { + let doc = editor.getValue().split("\n"); + // just remove the current line to prevent yaml parsing issues + doc.splice(cursor.line, 1); + doc = doc.slice(start + 1); + let end = doc.findIndex((l) => /^```$/.test(l)); + if (end < 0) end = doc.length; + + //parse as yaml so we can use this state later, e.g. to get already loaded players + this._encounter = parseYaml(doc.slice(0, end).join("\n")); + } catch (e) { + this._encounter = {}; + } + if (!this._encounter) this._encounter = {}; + + const line = editor.getLine(cursor.line); + //don't suggest anything for name + if (/^name/.test(line)) return null; + if (/^rollHP:/.test(line)) { + this._context = SuggestContext.RollHP; + const [_, query] = line.match(/^rollHP:\s?(.*)$/); + if (query === "true" || query === "false") return null; + return { + end: cursor, + start: { + ch: line.length - query.length, + line: cursor.line + }, + query + }; + } + if (/^party:/.test(line)) { + this._context = SuggestContext.Party; + const [_, query] = line.match(/^party:\s?(.*)$/); + if (this.plugin.data.parties.find((p) => p.name === query)) + return null; + return { + end: cursor, + start: { + ch: line.length - query.length, + line: cursor.line + }, + query + }; + } + if (/\s+- (?:\d:)?/.test(line)) { + //in creature or player context... try to figure out which + let found = false; + for (let i = split.length - 1; i >= 0; i--) { + let line = split[i]; + if (/^```$/.test(line)) return null; + if (/^```encounter/.test(line)) return null; + if (/^players:/.test(line)) { + this._context = SuggestContext.Players; + found = true; + break; + } + if (/^creatures:/.test(line)) { + this._context = SuggestContext.Creatures; + found = true; + break; + } + } + //panic + if (!found) return null; + + const [_, query] = line.match(/^\s+- (?:\d:)?(.*)$/); + return { + end: cursor, + start: { + ch: line.length - query.length, + line: cursor.line + }, + query + }; + } + return { + end: cursor, + start: { + ch: 0, + line: cursor.line + }, + query: line + }; + } +} diff --git a/src/main.ts b/src/main.ts index 842d86c8..88e55aec 100644 --- a/src/main.ts +++ b/src/main.ts @@ -34,6 +34,7 @@ import TrackerView, { CreatureView } from "./tracker/view"; import BuilderView from "./builder/view"; import PlayerView from "./tracker/player-view"; import { tracker } from "./tracker/stores/tracker"; +import { EncounterSuggester } from './encounter/editor-suggestor'; declare module "obsidian" { interface App { plugins: { @@ -238,6 +239,7 @@ export default class InitiativeTracker extends Plugin { this.addCommands(); this.addEvents(); + this.registerEditorSuggest(new EncounterSuggester(this)); this.registerMarkdownCodeBlockProcessor("encounter", (src, el, ctx) => { const handler = new EncounterBlock(this, src, el); ctx.addChild(handler);