diff --git a/package.json b/package.json index b70d614..03ceedf 100644 --- a/package.json +++ b/package.json @@ -100,6 +100,11 @@ "default": "# ${noteName}\n\n", "description": "Template for auto-created note files. Available tokens: ${noteName}, ${timestamp}. Timestamp is inserted in ISO format, i.e. 2020-07-09T05:29:00.541Z." }, + "vscodeMarkdownNotes.compileSuggestionDetails": { + "type": "boolean", + "default": false, + "description": "Specifies whether to compile the markdown content for details when showing suggestion details" + }, "vscodeMarkdownNotes.allowPipedWikiLinks": { "type": "boolean", "default": false, diff --git a/src/MarkdownFileCompletionItemProvider.ts b/src/MarkdownFileCompletionItemProvider.ts index 88f09b1..0d0dcf1 100644 --- a/src/MarkdownFileCompletionItemProvider.ts +++ b/src/MarkdownFileCompletionItemProvider.ts @@ -1,8 +1,16 @@ import * as vscode from 'vscode'; import { RefType, getRefAt } from './Ref'; import { NoteWorkspace } from './NoteWorkspace'; -import { NoteParser, Note } from './NoteParser'; +import { NoteParser } from './NoteParser'; +class MarkdownFileCompletionItem extends vscode.CompletionItem { + fsPath?: string; + + constructor(label: string, kind?: vscode.CompletionItemKind, fsPath?: string) { + super(label, kind); + this.fsPath = fsPath; + } +} // Given a document and position, check whether the current word matches one of // these 2 contexts: // 1. [[wiki-links]] @@ -34,7 +42,7 @@ export class MarkdownFileCompletionItemProvider implements vscode.CompletionItem return (await NoteWorkspace.noteFiles()).map((f) => { let kind = vscode.CompletionItemKind.File; let label = NoteWorkspace.wikiLinkCompletionForConvention(f, document); - let item = new vscode.CompletionItem(label, kind); + let item = new MarkdownFileCompletionItem(label, kind, f.fsPath); if (ref && ref.range) { item.range = ref.range; } @@ -44,4 +52,19 @@ export class MarkdownFileCompletionItemProvider implements vscode.CompletionItem return []; } } + + public async resolveCompletionItem( + item: MarkdownFileCompletionItem, + token: vscode.CancellationToken + ): Promise { + const fsPath = item.fsPath; + if (fsPath) { + let note = NoteParser.noteFromFsPath(fsPath); + if (note) { + item.detail = note.title?.text; + item.documentation = note.documentation(); + } + } + return item; + } } diff --git a/src/NoteParser.ts b/src/NoteParser.ts index 97d8f6e..ed89d0f 100644 --- a/src/NoteParser.ts +++ b/src/NoteParser.ts @@ -6,6 +6,7 @@ import { Ref, RefType } from './Ref'; import { NoteWorkspace } from './NoteWorkspace'; const RETURN_TYPE_VSCODE = 'vscode'; + type RawPosition = { line: number; character: number; @@ -56,6 +57,11 @@ export class Note { fsPath: string; data: string | undefined; refCandidates: Array = []; + title: { + text: string; + line: number; + contextLine: number; // line number after all empty lines + } | undefined; private _parsed: boolean = false; constructor(fsPath: string) { this.fsPath = fsPath; @@ -120,10 +126,30 @@ export class Note { // reset the refCandidates Array this.refCandidates = []; + let searchTitle = true; + let isSkip = false; let lines = this.data.split(/\r?\n/); lines.map((line, lineNum) => { + if (isSkip) { // ! skip all empty lines after title `# title` + if (line.trim() == '') { + that.title!.contextLine = lineNum; + } else { + isSkip = false; + } + } + if (searchTitle) { + Array.from(line.matchAll(NoteWorkspace.rxTitle())).map((match) => { + that.title = { + text: '# ' + match[0].trim(), + line: lineNum, + contextLine: lineNum + }; + searchTitle = false; // * only search for the first # h1 + isSkip = true; + }); + } Array.from(line.matchAll(NoteWorkspace.rxTagNoAnchors())).map((match) => { - + that.refCandidates.push(RefCandidate.fromMatch(lineNum, match, RefType.Tag)); }); Array.from(line.matchAll(NoteWorkspace.rxWikiLink()) || []).map((match) => { @@ -177,6 +203,28 @@ export class Note { }); return _tagSet; } + + // completionItem.documentation () + documentation(): string | vscode.MarkdownString | undefined { + if (this.data === undefined) { + return ""; + } else { + let data = this.data; + if (this.title) { // get the portion of the note after the title + data = this.data.split(/\r?\n/).slice(this.title.contextLine + 1).join('\n'); + } + if (NoteWorkspace.compileSuggestionDetails()) { + try { + let result = new vscode.MarkdownString(data); + return result; + } catch (error) { + return ""; + } + } else { + return data; + } + } + } } interface Dictionary { @@ -271,4 +319,8 @@ export class NoteParser { return locations; } + + static noteFromFsPath(fsPath: string): Note | undefined { + return this._notes[fsPath]; + } } diff --git a/src/NoteWorkspace.ts b/src/NoteWorkspace.ts index 92ba635..09e28cb 100644 --- a/src/NoteWorkspace.ts +++ b/src/NoteWorkspace.ts @@ -35,13 +35,13 @@ type Config = { slugifyCharacter: SlugifyCharacter; workspaceFilenameConvention: WorkspaceFilenameConvention; newNoteTemplate: string; + compileSuggestionDetails: boolean; triggerSuggestOnReplacement: boolean; allowPipedWikiLinks: boolean; pipedWikiLinksSyntax: PipedWikiLinksSyntax; pipedWikiLinksSeparator: string; newNoteDirectory: string; }; - // This class contains: // 1. an interface to some of the basic user configurable settings or this extension // 2. command for creating a New Note @@ -52,6 +52,7 @@ export class NoteWorkspace { static _rxTagNoAnchors = '\\#[\\w\\-\\_]+'; // used to match tags that appear within lines static _rxTagWithAnchors = '^\\#[\\w\\-\\_]+$'; // used to match entire words static _rxWikiLink = '\\[\\[[^sep\\]]+(sep[^sep\\]]+)?\\]\\]'; // [[wiki-link-regex(|with potential pipe)?]] Note: "sep" will be replaced with pipedWikiLinksSeparator on compile + static _rxTitle = '(?<=^( {0,3}#[^\\S\\r\\n]+)).+'; static _rxMarkdownWordPattern = '([\\_\\w\\#\\.\\/\\\\]+)'; // had to add [".", "/", "\"] to get relative path completion working and ["#"] to get tag completion working static _rxFileExtensions = '\\.(md|markdown|mdx|fountain)$'; static _defaultFileExtension = 'md'; @@ -64,6 +65,7 @@ export class NoteWorkspace { static _slugifyChar = '-'; static DEFAULT_CONFIG: Config = { createNoteOnGoToDefinitionWhenMissing: true, + compileSuggestionDetails: false, defaultFileExtension: NoteWorkspace._defaultFileExtension, noteCompletionConvention: NoteCompletionConvention.rawFilename, slugifyCharacter: SlugifyCharacter.dash, @@ -95,6 +97,7 @@ export class NoteWorkspace { 'workspaceFilenameConvention' ) as WorkspaceFilenameConvention, newNoteTemplate: c.get('newNoteTemplate') as string, + compileSuggestionDetails: c.get('compileSuggestionDetails') as boolean, triggerSuggestOnReplacement: c.get('triggerSuggestOnReplacement') as boolean, allowPipedWikiLinks: c.get('allowPipedWikiLinks') as boolean, pipedWikiLinksSyntax: c.get('pipedWikiLinksSyntax') as PipedWikiLinksSyntax, @@ -151,6 +154,9 @@ export class NoteWorkspace { this._rxWikiLink = this._rxWikiLink.replace(/sep/g, NoteWorkspace.pipedWikiLinksSeparator()); return new RegExp(this._rxWikiLink, 'gi'); } + static rxTitle(): RegExp { + return new RegExp(this._rxTitle, 'gi'); + } static rxMarkdownWordPattern(): RegExp { // return /([\#\.\/\\\w_]+)/; // had to add [".", "/", "\"] to get relative path completion working and ["#"] to get tag completion working return new RegExp(this._rxMarkdownWordPattern); @@ -195,6 +201,11 @@ export class NoteWorkspace { return !!this.cfg().createNoteOnGoToDefinitionWhenMissing; } + + static compileSuggestionDetails(): boolean { + return this.cfg().compileSuggestionDetails; + } + static stripExtension(noteName: string): string { return noteName.replace(NoteWorkspace.rxFileExtensions(), ''); }