Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions extension/src/help/documentationPresenter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,33 @@ export namespace Annotation {
lines.push(`@${MarkdownHelpers.italic('remarks')} — ${docs.remarks.value}`);
}

if (docs?.examples?.length > 0) {
for (const example of docs.examples) {
lines.push(MarkdownHelpers.divider);
const header = `@${MarkdownHelpers.italic('example')}`;
const nlIdx = example.value.indexOf('\n');
if (nlIdx < 0) {
lines.push(example.value ? `${header} — ${example.value}` : header);
} else {
const inlineDesc = example.value.substring(0, nlIdx);
const rest = example.value.substring(nlIdx + 1);
lines.push(inlineDesc ? `${header} — ${inlineDesc}` : header);
if (rest) {
lines.push('');
if (rest.trimStart().startsWith('```')) {
// Normalize only the opening fence line to use the autolisp language ID
const eol = rest.indexOf('\n');
const openLine = eol >= 0 ? rest.substring(0, eol) : rest;
const body = eol >= 0 ? rest.substring(eol) : '';
lines.push(openLine.replace(/^(\s*)```\w*/, '$1```autolisp') + body);
} else {
lines.push(`\`\`\`autolisp\n${rest}\n\`\`\``);
}
}
}
}
}

lines.push(MarkdownHelpers.divider);
lines.push(`${MarkdownHelpers.italic('source')} — ${path.basename(sourceFile)}`);

Expand Down
44 changes: 38 additions & 6 deletions extension/src/parsing/comments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export interface ILispDocs {
returns?: ILspDocPair;
description?: ILspDocPair;
remarks?: ILspDocPair;
examples?: Array<ILspDocPair>;
}

function normalizeComment(str: string) : string {
Expand All @@ -23,6 +24,12 @@ function normalizeComment(str: string) : string {
.trim();
}

function normalizeExampleLine(str: string): string {
// Strip up to 4 spaces of block-comment indentation while preserving all content,
// including legitimate AutoLISP comment characters (;) inside example code.
return str.replace(/^ {1,4}/, '');
}

/**
* This function extracts the user documentation into a basic structure for completion data
* @param value This should be a LispAtom that represents a comment or comment block
Expand All @@ -31,20 +38,34 @@ function normalizeComment(str: string) : string {
export function parseDocumentation(value: ILispFragment): ILispDocs {
const result: ILispDocs = { };
let active: ILspDocPair = null;
let inExample = false;
let inCodeFence = false;

if (!value.isComment()) {
return result;
}

const facets = value.symbol.replace(/\r\n/g, '\n').split('\n');
for (let i = 0; i < facets.length; i++) {
const line = normalizeComment(facets[i]);

// Track fenced code blocks to avoid mis-treating @ inside code as tags
if (inExample && normalizeExampleLine(facets[i]).trimStart().startsWith('```')) {
inCodeFence = !inCodeFence;
}

if (line === '' || line.toUpperCase().startsWith('@GLOBAL')) {
// Preserve blank lines within example content (but not comment delimiters like |;)
if (inExample && active && line === '' && facets[i].trim() === '') {
active.value += '\n';
}
continue;
}
if (line.startsWith('@') && line.includes(' ')) {
const first = line.substring(0, line.indexOf(' ')).toUpperCase();
const content = line.substring(first.length).trim();
if (!inCodeFence && line.startsWith('@') && (line.includes(' ') || line.toUpperCase().startsWith('@EXAMPLE'))) {
inExample = false;
const spaceIdx = line.indexOf(' ');
const first = spaceIdx >= 0 ? line.substring(0, spaceIdx).toUpperCase() : line.toUpperCase();
const content = spaceIdx >= 0 ? line.substring(first.length).trim() : '';
if (first.startsWith('@REMARK')) {
result['remarks'] = active = { name: 'Remarks', value: content };
} else if (first.startsWith('@DESC')) {
Expand All @@ -54,16 +75,27 @@ export function parseDocumentation(value: ILispFragment): ILispDocs {
} else if (first.startsWith('@PARAM')) {
if (!result['params']) {
result['params'] = [];
}
}
result['params'].push(active = { name: 'Param', value: content });
} else if (first.startsWith('@EXAMPLE')) {
if (!result['examples']) {
result['examples'] = [];
}
result['examples'].push(active = { name: 'Example', value: content });
inExample = true;
inCodeFence = false;
} else {
// if it started with @ and its not roughly a predesignated @TYPE, then
// do nothing and stop the process that assembles the previous @TYPE
active = null;
}
} else {
if (active) {
active.value += ' ' + line;
if (inExample) {
active.value += '\n' + normalizeExampleLine(facets[i]);
} else {
active.value += ' ' + line;
}
} else if (!result['description']) {
// this handles implied short descriptions as the first available content
result['description'] = active = { name: 'Description', value: line };
Expand Down
67 changes: 67 additions & 0 deletions extension/src/test/suite/parsing.comments.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ import { Position } from 'vscode';
import { getBlockCommentParamNameRange, parseDocumentation } from '../../parsing/comments';
import { ReadonlyDocument } from '../../project/readOnlyDocument';

function memDoc(content: string): ReadonlyDocument {
return ReadonlyDocument.createMemoryDocument(content, 'autolisp');
}


suite("Parsing: Comments Tests", function () {

Expand Down Expand Up @@ -82,4 +86,67 @@ suite("Parsing: Comments Tests", function () {
});


test("@Example tag with inline description and fenced code block", function () {
const src = ';|\n @Example How to use it\n ```lisp\n (f 2)\n ```\n|;';
const doc = parseDocumentation(memDoc(src).documentContainer.atoms[0]);
expect(doc.examples).to.not.be.undefined;
expect(doc.examples.length).to.equal(1);
expect(doc.examples[0].value).to.include('How to use it');
expect(doc.examples[0].value).to.include('```lisp');
expect(doc.examples[0].value).to.include('(f 2)');
});

test("@Example tag with no inline description", function () {
const src = ';|\n @Example\n ```lisp\n (hello)\n ```\n|;';
const doc = parseDocumentation(memDoc(src).documentContainer.atoms[0]);
expect(doc.examples).to.not.be.undefined;
expect(doc.examples.length).to.equal(1);
expect(doc.examples[0].value).to.include('```lisp');
expect(doc.examples[0].value).to.include('(hello)');
});

test("Multiple @Example tags", function () {
const src = ';|\n @Example First example\n ```lisp\n (f 1)\n ```\n @Example Second example\n ```lisp\n (f 2)\n ```\n|;';
const doc = parseDocumentation(memDoc(src).documentContainer.atoms[0]);
expect(doc.examples).to.not.be.undefined;
expect(doc.examples.length).to.equal(2);
expect(doc.examples[0].value).to.include('First example');
expect(doc.examples[1].value).to.include('Second example');
});

test("@Example alongside @Param, @Returns, @Remarks", function () {
const src = ';|\n A test function\n @Param x int: the input\n @Returns int\n @Remarks some notes\n @Example usage\n ```lisp\n (myfn 5)\n ```\n|;';
const doc = parseDocumentation(memDoc(src).documentContainer.atoms[0]);
expect(doc.description.value).to.include('A test function');
expect(doc.params.length).to.equal(1);
expect(doc.returns.value).to.include('int');
expect(doc.remarks.value).to.include('some notes');
expect(doc.examples.length).to.equal(1);
expect(doc.examples[0].value).to.include('usage');
expect(doc.examples[0].value).to.include('(myfn 5)');
});

test("@Example one-liner with no code block", function () {
const src = ';|\n @Example call it like `(f 2)`\n|;';
const doc = parseDocumentation(memDoc(src).documentContainer.atoms[0]);
expect(doc.examples).to.not.be.undefined;
expect(doc.examples.length).to.equal(1);
expect(doc.examples[0].value).to.equal('call it like `(f 2)`');
});

test("@Example preserves leading semicolons in example code", function () {
const src = ';|\n @Example usage\n ; this is a lisp comment\n (f 2) ; inline comment\n|;';
const doc = parseDocumentation(memDoc(src).documentContainer.atoms[0]);
expect(doc.examples[0].value).to.include('; this is a lisp comment');
expect(doc.examples[0].value).to.include('(f 2) ; inline comment');
});

test("@Example with @ inside code fence is not treated as a tag", function () {
const src = ';|\n @Example code with at-sign\n ```lisp\n @somevar\n ```\n|;';
const doc = parseDocumentation(memDoc(src).documentContainer.atoms[0]);
expect(doc.examples.length).to.equal(1);
expect(doc.examples[0].value).to.include('@somevar');
});


});
40 changes: 40 additions & 0 deletions extension/src/test/suite/providers.hoverProvider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,46 @@ suite("Providers: Hover", function () {
});


test("UserDefined LSP @Example one-liner - Markdown Verification", function () {
try {
const src = ';|\n @Returns int\n @Example call it like `(double 5)`\n|;\n(defun double (x) (* 2 x))';
const exDoc = ReadonlyDocument.createMemoryDocument(src, DocumentServices.Selectors.LSP);
const docs = parseDocumentation(exDoc.documentContainer.atoms[0]);
const flatView = exDoc.documentContainer.flatten();
const defunAtom = flatView.find(a => a.symbol === 'double');
const md = Annotation.asMarkdown(defunAtom, -1, [], docs, 'test.lsp');
expect(md.value).to.include('@*example*');
expect(md.value).to.include('call it like `(double 5)`');
// one-liner: no code block should be injected
expect(md.value).to.not.include('```autolisp');
}
catch (err) {
assert.fail(`@Example one-liner hover rendering failed: ${err}`);
}
});


test("UserDefined LSP @Example - Markdown Verification", function () {
try {
const src = ';|\n A documented function\n @Param x int: the input\n @Returns int\n @Example Double the input\n ```lisp\n (double 5)\n ```\n|;\n(defun double (x) (* 2 x))';
const exDoc = ReadonlyDocument.createMemoryDocument(src, DocumentServices.Selectors.LSP);
const commentAtom = exDoc.documentContainer.atoms[0];
const docs = parseDocumentation(commentAtom);
const flatView = exDoc.documentContainer.flatten();
const defunAtom = flatView.find(a => a.symbol === 'double');
expect(defunAtom).to.not.be.undefined;
const md = Annotation.asMarkdown(defunAtom, -1, [], docs, 'test.lsp');
expect(md.value).to.include('@*example*');
expect(md.value).to.include('Double the input');
expect(md.value).to.include('```autolisp');
expect(md.value).to.include('(double 5)');
}
catch (err) {
assert.fail(`@Example hover rendering failed: ${err}`);
}
});


test("UserDefined LSP ActiveIndex - Markdown Verification", function () {
let passedCount = 0;
try {
Expand Down
2 changes: 1 addition & 1 deletion extension/syntaxes/autolisp.tmLanguage.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
"patterns": [
{
"name": "string.regexp",
"begin": "^\\s*(?i)(@PARAM|@REMARKS|@DESCRIPTION|@RETURNS)(?=\\s.+)",
"begin": "^\\s*(?i)(@PARAM|@REMARKS|@DESCRIPTION|@RETURNS|@EXAMPLES?)(?=\\s|$)",
"end": "(\\n|\\s)"
},
{
Expand Down
Loading