Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Properly account for new TSX prefix #733

Merged
merged 13 commits into from
Jan 5, 2024
2 changes: 1 addition & 1 deletion packages/language-server/src/core/astro2tsx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ function getVirtualFileTSX(
semantic: true,
navigation: true,
structure: true,
format: true,
format: false,
}
: {
verification: false,
Expand Down
10 changes: 5 additions & 5 deletions packages/language-server/src/core/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import type { DiagnosticMessage, ParseResult } from '@astrojs/compiler/types';
import type { DiagnosticMessage } from '@astrojs/compiler/types';
import type { LanguagePlugin, VirtualFile } from '@volar/language-core';
import * as path from 'node:path';
import type ts from 'typescript/lib/tsserverlibrary';
import type { HTMLDocument } from 'vscode-html-languageservice';
import type { AstroInstall } from '../utils.js';
import { astro2tsx } from './astro2tsx';
import { FrontmatterStatus, getAstroMetadata } from './parseAstro';
import { AstroMetadata, getAstroMetadata } from './parseAstro';
import { extractStylesheets } from './parseCSS';
import { parseHTML } from './parseHTML';
import { extractScriptTags } from './parseJS.js';
Expand Down Expand Up @@ -61,9 +61,9 @@ export function getLanguageModule(
module: ts.ModuleKind.ESNext ?? 99,
target: ts.ScriptTarget.ESNext ?? 99,
jsx: ts.JsxEmit.Preserve ?? 1,
jsxImportSource: undefined,
jsxFactory: 'astroHTML',
// Always true for Astro
resolveJsonModule: true,
// Necessary for inline script tags that are JavaScript
allowJs: true,
isolatedModules: true,
moduleResolution:
Expand All @@ -84,7 +84,7 @@ export class AstroFile implements VirtualFile {
languageId = 'astro';
mappings!: VirtualFile['mappings'];
embeddedFiles!: VirtualFile['embeddedFiles'];
astroMeta!: ParseResult & { frontmatter: FrontmatterStatus };
astroMeta!: AstroMetadata;
compilerDiagnostics!: DiagnosticMessage[];
htmlDocument!: HTMLDocument;
scriptFiles!: string[];
Expand Down
4 changes: 3 additions & 1 deletion packages/language-server/src/core/parseAstro.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { parse } from '@astrojs/compiler/sync';
import type { ParseOptions, ParseResult, Point } from '@astrojs/compiler/types';

type AstroMetadata = ParseResult & { frontmatter: FrontmatterStatus };
export type AstroMetadata = ParseResult & { frontmatter: FrontmatterStatus; tsxStartLine: number };

export function getAstroMetadata(
fileName: string,
Expand All @@ -13,6 +13,8 @@ export function getAstroMetadata(
return {
...parseResult,
frontmatter: getFrontmatterStatus(parseResult.ast, input),
// TODO: The compiler could probably return the TSX start line, but for now we'll just assume it's always 1
tsxStartLine: 1,
};
}

Expand Down
28 changes: 20 additions & 8 deletions packages/language-server/src/plugins/typescript/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ServicePluginInstance, ServicePlugin, TextDocumentEdit } from '@volar/language-server';
import { ServicePlugin, ServicePluginInstance, TextDocumentEdit } from '@volar/language-server';
import { create as createTypeScriptService } from 'volar-service-typescript';
import { AstroFile } from '../../core/index.js';
import {
Expand Down Expand Up @@ -37,8 +37,8 @@ export const create = (ts: typeof import('typescript/lib/tsserverlibrary.js')):
edit.newText.replace('import type', 'import');
}

if (editShouldBeInFrontmatter(edit.range)) {
return ensureProperEditForFrontmatter(edit, file.astroMeta.frontmatter, newLine);
if (editShouldBeInFrontmatter(edit.range, file.astroMeta.tsxStartLine).itShould) {
return ensureProperEditForFrontmatter(edit, file.astroMeta, newLine);
}

return edit;
Expand Down Expand Up @@ -78,11 +78,15 @@ export const create = (ts: typeof import('typescript/lib/tsserverlibrary.js')):
change.textDocument.uri = originalUri;
if (change.edits.length === 1) {
change.edits = change.edits.map((edit) => {
const editInFrontmatter = editShouldBeInFrontmatter(edit.range, document);
const editInFrontmatter = editShouldBeInFrontmatter(
edit.range,
file.astroMeta.tsxStartLine,
document
);
if (editInFrontmatter.itShould) {
return ensureProperEditForFrontmatter(
edit,
file.astroMeta.frontmatter,
file.astroMeta,
newLine,
editInFrontmatter.position
);
Expand All @@ -93,11 +97,15 @@ export const create = (ts: typeof import('typescript/lib/tsserverlibrary.js')):
} else {
if (file.astroMeta.frontmatter.status === 'closed') {
change.edits = change.edits.map((edit) => {
const editInFrontmatter = editShouldBeInFrontmatter(edit.range, document);
const editInFrontmatter = editShouldBeInFrontmatter(
edit.range,
file.astroMeta.tsxStartLine,
document
);
if (editInFrontmatter.itShould) {
edit.range = ensureRangeIsInFrontmatter(
edit.range,
file.astroMeta.frontmatter,
file.astroMeta,
editInFrontmatter.position
);
}
Expand All @@ -107,7 +115,11 @@ export const create = (ts: typeof import('typescript/lib/tsserverlibrary.js')):
// TODO: Handle when there's multiple edits and a new frontmatter is potentially needed
if (
change.edits.some((edit) => {
return editShouldBeInFrontmatter(edit.range, document).itShould;
return editShouldBeInFrontmatter(
edit.range,
file.astroMeta.tsxStartLine,
document
).itShould;
})
) {
console.error(
Expand Down
43 changes: 26 additions & 17 deletions packages/language-server/src/plugins/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { HTMLDocument, Node, Range, TextDocument, TextEdit } from 'vscode-html-languageservice';
import type { FrontmatterStatus } from '../core/parseAstro.js';
import type { AstroMetadata, FrontmatterStatus } from '../core/parseAstro.js';

export function isJSDocument(languageId: string) {
return (
Expand Down Expand Up @@ -52,17 +52,22 @@ type FrontmatterEditPosition = 'top' | 'bottom';

export function ensureProperEditForFrontmatter(
edit: TextEdit,
frontmatter: FrontmatterStatus,
metadata: AstroMetadata,
newLine: string,
position: FrontmatterEditPosition = 'top'
): TextEdit {
switch (frontmatter.status) {
switch (metadata.frontmatter.status) {
case 'open':
return getOpenFrontmatterEdit(edit, newLine);
case 'closed':
const newRange = ensureRangeIsInFrontmatter(edit.range, metadata, position);
return {
newText: edit.newText,
range: ensureRangeIsInFrontmatter(edit.range, frontmatter, position),
newText:
newRange.start.line === metadata.frontmatter.position.start.line &&
edit.newText.startsWith(newLine)
? edit.newText.trimStart()
: edit.newText,
range: newRange,
};
case 'doesnt-exist':
return getNewFrontmatterEdit(edit, newLine);
Expand All @@ -74,25 +79,26 @@ export function ensureProperEditForFrontmatter(
*/
export function ensureRangeIsInFrontmatter(
range: Range,
frontmatter: FrontmatterStatus,
metadata: AstroMetadata,
position: FrontmatterEditPosition = 'top'
): Range {
if (frontmatter.status === 'open' || frontmatter.status === 'closed') {
if (metadata.frontmatter.status === 'open' || metadata.frontmatter.status === 'closed') {
// Q: Why not use PointToPosition?
// A: The Astro compiler returns positions at the exact line where the frontmatter is, which is not adequate for mapping
// edits as we want edits *inside* the frontmatter and not on the same line, or you would end up with things like `---import ...`
const frontmatterStartPosition = {
line: frontmatter.position.start.line,
character: frontmatter.position.start.column - 1,
line: metadata.frontmatter.position.start.line,
character: metadata.frontmatter.position.start.column - 1,
};
const frontmatterEndPosition = frontmatter.position.end
? { line: frontmatter.position.end.line - 1, character: 0 }
const frontmatterEndPosition = metadata.frontmatter.position.end
? { line: metadata.frontmatter.position.end.line - 1, character: 0 }
: undefined;

// If the range start is outside the frontmatter, return a range at the start of the frontmatter
const adjustedStartLine = range.start.line - metadata.tsxStartLine;
if (
range.start.line < frontmatterStartPosition.line ||
(frontmatterEndPosition && range.start.line > frontmatterEndPosition.line)
adjustedStartLine < frontmatterStartPosition.line ||
(frontmatterEndPosition && adjustedStartLine > frontmatterEndPosition.line)
) {
if (frontmatterEndPosition && position === 'bottom') {
return Range.create(frontmatterEndPosition, frontmatterEndPosition);
Expand All @@ -108,7 +114,9 @@ export function ensureRangeIsInFrontmatter(
}

export function getNewFrontmatterEdit(edit: TextEdit, newLine: string) {
edit.newText = `---${newLine}${edit.newText}---${newLine}${newLine}`;
edit.newText = `---${edit.newText.startsWith(newLine) ? '' : newLine}${
edit.newText
}---${newLine}${newLine}`;
edit.range = Range.create(0, 0, 0, 0);

return edit;
Expand All @@ -125,15 +133,16 @@ type FrontmatterEditValidity =
| { itShould: false; position: undefined }
| { itShould: true; position: FrontmatterEditPosition };

// Most edits that are at 0:0, or outside the document are intended for the frontmatter
// Most edits that are at the beginning of the TSX, or outside the document are intended for the frontmatter
export function editShouldBeInFrontmatter(
range: Range,
tsxStartLine: number,
astroDocument?: TextDocument
): FrontmatterEditValidity {
const isAtZeroZero = range.start.line === 0 && range.start.character === 0;
const isAtTSXStart = range.start.line === tsxStartLine && range.start.character === 0;

const isPastFile = astroDocument && range.start.line > astroDocument.lineCount;
const shouldIt = isAtZeroZero || isPastFile;
const shouldIt = isAtTSXStart || isPastFile;

return shouldIt
? { itShould: true, position: isPastFile ? 'bottom' : 'top' }
Expand Down
20 changes: 10 additions & 10 deletions packages/language-server/test/units/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,20 +77,20 @@ describe('Utilities', async () => {

it('ensureRangeIsInFrontmatter - properly return a range inside the frontmatter', () => {
const beforeFrontmatterRange = html.Range.create(0, 0, 0, 0);
const hasFrontmatter = getAstroMetadata('file.astro', '---\nfoo\n---\n');
expect(
utils.ensureRangeIsInFrontmatter(beforeFrontmatterRange, hasFrontmatter.frontmatter)
).to.deep.equal(Range.create(1, 0, 1, 0));
const astroMetadata = getAstroMetadata('file.astro', '---\nfoo\n---\n');
expect(utils.ensureRangeIsInFrontmatter(beforeFrontmatterRange, astroMetadata)).to.deep.equal(
Range.create(1, 0, 1, 0)
);

const insideFrontmatterRange = html.Range.create(1, 0, 1, 0);
expect(
utils.ensureRangeIsInFrontmatter(insideFrontmatterRange, hasFrontmatter.frontmatter)
).to.deep.equal(Range.create(1, 0, 1, 0));
expect(utils.ensureRangeIsInFrontmatter(insideFrontmatterRange, astroMetadata)).to.deep.equal(
Range.create(1, 0, 1, 0)
);

const outsideFrontmatterRange = html.Range.create(6, 0, 6, 0);
expect(
utils.ensureRangeIsInFrontmatter(outsideFrontmatterRange, hasFrontmatter.frontmatter)
).to.deep.equal(Range.create(1, 0, 1, 0));
expect(utils.ensureRangeIsInFrontmatter(outsideFrontmatterRange, astroMetadata)).to.deep.equal(
Range.create(1, 0, 1, 0)
);
});

it('getNewFrontmatterEdit - properly return a new frontmatter edit', () => {
Expand Down
Loading