diff --git a/extensions/vscode-vue-language-features/languages/markdown-language-configuration.json b/extensions/vscode-vue-language-features/languages/markdown-language-configuration.json new file mode 100644 index 000000000..c75dce6cc --- /dev/null +++ b/extensions/vscode-vue-language-features/languages/markdown-language-configuration.json @@ -0,0 +1,55 @@ +{ + "autoClosingPairs": [ + // html + { + "open": "{", + "close": "}" + }, + { + "open": "[", + "close": "]" + }, + { + "open": "(", + "close": ")" + }, + { + "open": "'", + "close": "'" + }, + { + "open": "\"", + "close": "\"" + }, + { + "open": "", + "notIn": [ + "comment", + "string" + ] + }, + // javascript + { + "open": "`", + "close": "`", + "notIn": [ + "string", + "comment" + ] + }, + { + "open": "/**", + "close": " */", + "notIn": [ + "string" + ] + } + ], + "colorizedBracketPairs": [ + [ + "{{", + "}}" + ], + ] +} \ No newline at end of file diff --git a/extensions/vscode-vue-language-features/package.json b/extensions/vscode-vue-language-features/package.json index 95a31e45d..b135cb72e 100644 --- a/extensions/vscode-vue-language-features/package.json +++ b/extensions/vscode-vue-language-features/package.json @@ -25,6 +25,7 @@ ], "activationEvents": [ "onLanguage:vue", + "onLanguage:markdown", "onLanguage:javascript", "onLanguage:typescript", "onLanguage:javascriptreact", @@ -45,6 +46,11 @@ } }, "contributes": { + "configurationDefaults": { + "[markdown]": { + "editor.quickSuggestions": true + } + }, "jsonValidation": [ { "fileMatch": "tsconfig.json", @@ -119,6 +125,10 @@ ], "configuration": "./languages/vue-language-configuration.json" }, + { + "id": "markdown", + "configuration": "./languages/markdown-language-configuration.json" + }, { "id": "html", "configuration": "./languages/sfc-template-language-configuration.json" @@ -450,6 +460,38 @@ "support.class.component.vue" ] } + }, + { + "language": "markdown", + "scopes": { + "property": [ + "variable.other.property.vue" + ], + "property.readonly": [ + "variable.other.constant.property.vue" + ], + "variable": [ + "variable.other.readwrite.vue" + ], + "variable.readonly": [ + "variable.other.constant.object.vue" + ], + "function": [ + "entity.name.function.vue" + ], + "namespace": [ + "entity.name.type.module.vue" + ], + "variable.defaultLibrary": [ + "support.variable.vue" + ], + "function.defaultLibrary": [ + "support.function.vue" + ], + "componentTag": [ + "support.class.component.vue" + ] + } } ], "commands": [ diff --git a/extensions/vscode-vue-language-features/src/browserClientMain.ts b/extensions/vscode-vue-language-features/src/browserClientMain.ts index 898597519..8094361aa 100644 --- a/extensions/vscode-vue-language-features/src/browserClientMain.ts +++ b/extensions/vscode-vue-language-features/src/browserClientMain.ts @@ -17,7 +17,7 @@ export function activate(context: vscode.ExtensionContext) { initializationOptions: initOptions, progressOnInitialization: true, synchronize: { - fileEvents: vscode.workspace.createFileSystemWatcher('{**/*.vue,**/*.js,**/*.jsx,**/*.ts,**/*.tsx,**/*.json}') + fileEvents: vscode.workspace.createFileSystemWatcher('{**/*.vue,**/*.md,**/*.js,**/*.jsx,**/*.ts,**/*.tsx,**/*.json}') } }; const client = new lsp.LanguageClient( diff --git a/extensions/vscode-vue-language-features/src/common.ts b/extensions/vscode-vue-language-features/src/common.ts index 537d9d8f6..aac804f5e 100644 --- a/extensions/vscode-vue-language-features/src/common.ts +++ b/extensions/vscode-vue-language-features/src/common.ts @@ -45,7 +45,7 @@ export async function activate(context: vscode.ExtensionContext, createLc: Creat } const currentlangId = vscode.window.activeTextEditor.document.languageId; - if (currentlangId === 'vue') { + if (currentlangId === 'vue' || currentlangId === 'markdown') { doActivate(context, createLc); stopCheck.dispose(); } @@ -66,6 +66,7 @@ async function doActivate(context: vscode.ExtensionContext, createLc: CreateLang const languageFeaturesDocumentSelector: lsp.DocumentSelector = takeOverMode ? [ { scheme: 'file', language: 'vue' }, + { scheme: 'file', language: 'markdown' }, { scheme: 'file', language: 'javascript' }, { scheme: 'file', language: 'typescript' }, { scheme: 'file', language: 'javascriptreact' }, @@ -73,16 +74,19 @@ async function doActivate(context: vscode.ExtensionContext, createLc: CreateLang { scheme: 'file', language: 'json' }, ] : [ { scheme: 'file', language: 'vue' }, + { scheme: 'file', language: 'markdown' }, ]; const documentFeaturesDocumentSelector: lsp.DocumentSelector = takeOverMode ? [ { language: 'vue' }, + { language: 'markdown' }, { language: 'javascript' }, { language: 'typescript' }, { language: 'javascriptreact' }, { language: 'typescriptreact' }, ] : [ { language: 'vue' }, + { language: 'markdown' }, ]; const _useSecondServer = useSecondServer(); const _serverMaxOldSpaceSize = serverMaxOldSpaceSize(); diff --git a/extensions/vscode-vue-language-features/src/features/attrNameCase.ts b/extensions/vscode-vue-language-features/src/features/attrNameCase.ts index 1e5742ff2..3ff023c60 100644 --- a/extensions/vscode-vue-language-features/src/features/attrNameCase.ts +++ b/extensions/vscode-vue-language-features/src/features/attrNameCase.ts @@ -67,7 +67,7 @@ export async function activate(context: vscode.ExtensionContext, languageClient: }; async function onChangeDocument(newDoc: vscode.TextDocument | undefined) { - if (newDoc?.languageId === 'vue') { + if (newDoc?.languageId === 'vue' || newDoc?.languageId === 'markdown') { let attrCase = attrCases.uriGet(newDoc.uri.toString()); if (!attrCase) { const attrMode = vscode.workspace.getConfiguration('volar').get<'auto-kebab' | 'auto-camel' | 'kebab' | 'camel'>('completion.preferredAttrNameCase'); diff --git a/extensions/vscode-vue-language-features/src/features/autoInsertion.ts b/extensions/vscode-vue-language-features/src/features/autoInsertion.ts index c9368410f..e288e843f 100644 --- a/extensions/vscode-vue-language-features/src/features/autoInsertion.ts +++ b/extensions/vscode-vue-language-features/src/features/autoInsertion.ts @@ -6,6 +6,7 @@ export async function activate(context: vscode.ExtensionContext, htmlClient: Bas const supportedLanguages: Record = { vue: true, + markdown: true, javascript: true, typescript: true, javascriptreact: true, diff --git a/extensions/vscode-vue-language-features/src/features/tagNameCase.ts b/extensions/vscode-vue-language-features/src/features/tagNameCase.ts index 071cabb64..e5f9f8931 100644 --- a/extensions/vscode-vue-language-features/src/features/tagNameCase.ts +++ b/extensions/vscode-vue-language-features/src/features/tagNameCase.ts @@ -97,7 +97,7 @@ export async function activate(context: vscode.ExtensionContext, languageClient: }; async function onChangeDocument(newDoc: vscode.TextDocument | undefined) { - if (newDoc?.languageId === 'vue') { + if (newDoc?.languageId === 'vue' || newDoc?.languageId === 'markdown') { let tagCase = tagCases.uriGet(newDoc.uri.toString()); if (!tagCase) { const tagMode = vscode.workspace.getConfiguration('volar').get<'auto' | 'both' | 'kebab' | 'pascal'>('completion.preferredTagNameCase'); diff --git a/extensions/vscode-vue-language-features/src/features/tsVersion.ts b/extensions/vscode-vue-language-features/src/features/tsVersion.ts index 8ab92e878..a38801b78 100644 --- a/extensions/vscode-vue-language-features/src/features/tsVersion.ts +++ b/extensions/vscode-vue-language-features/src/features/tsVersion.ts @@ -87,6 +87,7 @@ export async function activate(context: vscode.ExtensionContext, clients: BaseLa function updateStatusBar() { if ( vscode.window.activeTextEditor?.document.languageId !== 'vue' + && vscode.window.activeTextEditor?.document.languageId !== 'markdown' && !( takeOverModeEnabled() && vscode.window.activeTextEditor diff --git a/extensions/vscode-vue-language-features/src/features/tsconfig.ts b/extensions/vscode-vue-language-features/src/features/tsconfig.ts index ecda0632b..3b1b6cb63 100644 --- a/extensions/vscode-vue-language-features/src/features/tsconfig.ts +++ b/extensions/vscode-vue-language-features/src/features/tsconfig.ts @@ -20,6 +20,7 @@ export async function activate(context: vscode.ExtensionContext, languageClient: async function updateStatusBar() { if ( vscode.window.activeTextEditor?.document.languageId !== 'vue' + && vscode.window.activeTextEditor?.document.languageId !== 'markdown' && !( takeOverModeEnabled() && vscode.window.activeTextEditor diff --git a/extensions/vscode-vue-language-features/src/nodeClientMain.ts b/extensions/vscode-vue-language-features/src/nodeClientMain.ts index 79cea850f..2d5f8a881 100644 --- a/extensions/vscode-vue-language-features/src/nodeClientMain.ts +++ b/extensions/vscode-vue-language-features/src/nodeClientMain.ts @@ -35,7 +35,7 @@ export function activate(context: vscode.ExtensionContext) { initializationOptions: initOptions, progressOnInitialization: true, synchronize: { - fileEvents: vscode.workspace.createFileSystemWatcher('{**/*.vue,**/*.js,**/*.jsx,**/*.ts,**/*.tsx,**/*.json}') + fileEvents: vscode.workspace.createFileSystemWatcher('{**/*.vue,**/*.md,**/*.js,**/*.jsx,**/*.ts,**/*.tsx,**/*.json}') } }; const client = new lsp.LanguageClient( diff --git a/packages/shared/src/common.ts b/packages/shared/src/common.ts index 235f46a7e..b831644c6 100644 --- a/packages/shared/src/common.ts +++ b/packages/shared/src/common.ts @@ -13,6 +13,7 @@ export function syntaxToLanguageId(syntax: string) { case 'jsx': return 'javascriptreact'; case 'tsx': return 'typescriptreact'; case 'pug': return 'jade'; + case 'md': return 'markdown'; } return syntax; } @@ -24,6 +25,7 @@ export function languageIdToSyntax(languageId: string) { case 'javascriptreact': return 'jsx'; case 'typescriptreact': return 'tsx'; case 'jade': return 'pug'; + case 'markdown': return 'md'; } return languageId; } diff --git a/packages/typescript-vue-plugin/src/index.ts b/packages/typescript-vue-plugin/src/index.ts index b3311faad..022c35c07 100644 --- a/packages/typescript-vue-plugin/src/index.ts +++ b/packages/typescript-vue-plugin/src/index.ts @@ -19,6 +19,7 @@ const init: ts.server.PluginModuleFactory = (modules) => { info.project.getScriptKind = fileName => { switch (path.extname(fileName)) { case '.vue': return ts.ScriptKind.TSX; // can't use External, Unknown + case '.md': return ts.ScriptKind.TSX; // can't use External, Unknown case '.js': return ts.ScriptKind.JS; case '.jsx': return ts.ScriptKind.JSX; case '.ts': return ts.ScriptKind.TS; @@ -158,7 +159,7 @@ function createProxyHost(ts: typeof import('typescript/lib/tsserverlibrary'), in }; async function onAnyDriveFileUpdated(fileName: string) { - if (fileName.endsWith('.vue') && info.project.fileExists(fileName) && !vueFiles.has(fileName)) { + if ((fileName.endsWith('.vue') || fileName.endsWith('.md')) && info.project.fileExists(fileName) && !vueFiles.has(fileName)) { onConfigUpdated(); } } @@ -202,7 +203,7 @@ function createProxyHost(ts: typeof import('typescript/lib/tsserverlibrary'), in const parseConfigHost: ts.ParseConfigHost = { useCaseSensitiveFileNames: info.project.useCaseSensitiveFileNames(), readDirectory: (path, extensions, exclude, include, depth) => { - return info.project.readDirectory(path, ['.vue'], exclude, include, depth); + return info.project.readDirectory(path, ['.vue', '.md'], exclude, include, depth); }, fileExists: fileName => info.project.fileExists(fileName), readFile: fileName => info.project.readFile(fileName), diff --git a/packages/vue-code-gen/src/generators/template.ts b/packages/vue-code-gen/src/generators/template.ts index 343e1d1e4..0140af9c8 100644 --- a/packages/vue-code-gen/src/generators/template.ts +++ b/packages/vue-code-gen/src/generators/template.ts @@ -65,7 +65,7 @@ export function generate( allowTypeNarrowingInEventExpressions: boolean, hasScriptSetup: boolean, cssScopedClasses: string[] = [], - htmlToTemplate: (htmlStart: number, htmlEnd: number) => { start: number, end: number; } | undefined, + htmlToTemplate: (htmlRange: { start: number, end: number; }) => { start: number, end: number; } | undefined, searchTexts: { getEmitCompletion(tag: string): string, getPropsCompletion(tag: string): string, @@ -228,7 +228,7 @@ export function generate( tagResolves[tagName] = { component: var_rawComponent, emit: var_emit, - offsets: tagOffsets.map(offset => htmlToTemplate(offset, offset)?.start).filter(notEmpty), + offsets: tagOffsets.map(offset => htmlToTemplate({ start: offset, end: offset })?.start).filter(notEmpty), }; } @@ -1182,7 +1182,7 @@ export function generate( end: prop.arg.loc.start.offset + end, }; - const newStart = htmlToTemplate(sourceRange.start, sourceRange.end)?.start; + const newStart = htmlToTemplate({ start: sourceRange.start, end: sourceRange.end })?.start; if (newStart === undefined) continue; const offset = newStart - sourceRange.start; sourceRange.start += offset; @@ -1847,7 +1847,7 @@ export function generate( function addMapping(gen: typeof tsCodeGen, mapping: SourceMaps.Mapping) { const newMapping = { ...mapping }; - const templateStart = htmlToTemplate(mapping.sourceRange.start, mapping.sourceRange.end)?.start; + const templateStart = htmlToTemplate(mapping.sourceRange)?.start; if (templateStart === undefined) return; // not found const offset = templateStart - mapping.sourceRange.start; newMapping.sourceRange = { @@ -1858,7 +1858,7 @@ export function generate( if (mapping.additional) { newMapping.additional = []; for (const other of mapping.additional) { - let otherTemplateStart = htmlToTemplate(other.sourceRange.start, other.sourceRange.end)?.start; + let otherTemplateStart = htmlToTemplate(other.sourceRange)?.start; if (otherTemplateStart === undefined) continue; const otherOffset = otherTemplateStart - other.sourceRange.start; newMapping.additional.push({ diff --git a/packages/vue-language-server/src/features/languageFeatures.ts b/packages/vue-language-server/src/features/languageFeatures.ts index 7da550b22..b8b59f1db 100644 --- a/packages/vue-language-server/src/features/languageFeatures.ts +++ b/packages/vue-language-server/src/features/languageFeatures.ts @@ -279,7 +279,7 @@ export function register( }); connection.workspace.onWillRenameFiles(async handler => { - const hasTsFile = handler.files.some(file => file.newUri.endsWith('.vue') || file.newUri.endsWith('.ts') || file.newUri.endsWith('.tsx')); + const hasTsFile = handler.files.some(file => file.newUri.endsWith('.vue') || file.newUri.endsWith('.md') || file.newUri.endsWith('.ts') || file.newUri.endsWith('.tsx')); const config: 'prompt' | 'always' | 'never' | null | undefined = await connection.workspace.getConfiguration(hasTsFile ? 'typescript.updateImportsOnFileMove.enabled' : 'javascript.updateImportsOnFileMove.enabled'); if (config === 'always') { diff --git a/packages/vue-language-server/src/project.ts b/packages/vue-language-server/src/project.ts index dbf8e6206..ae3702e66 100644 --- a/packages/vue-language-server/src/project.ts +++ b/packages/vue-language-server/src/project.ts @@ -243,7 +243,7 @@ export async function createProject( const parseConfigHost: ts.ParseConfigHost = { useCaseSensitiveFileNames: projectSys.useCaseSensitiveFileNames, readDirectory: (path, extensions, exclude, include, depth) => { - return projectSys.readDirectory(path, [...extensions, '.vue'], exclude, include, depth); + return projectSys.readDirectory(path, [...extensions, '.vue', '.md'], exclude, include, depth); }, fileExists: projectSys.fileExists, readFile: projectSys.readFile, diff --git a/packages/vue-language-server/src/registers/registerlanguageFeatures.ts b/packages/vue-language-server/src/registers/registerlanguageFeatures.ts index ea31ff78f..2fe9f4d65 100644 --- a/packages/vue-language-server/src/registers/registerlanguageFeatures.ts +++ b/packages/vue-language-server/src/registers/registerlanguageFeatures.ts @@ -36,6 +36,7 @@ export function register( willRename: { filters: [ { pattern: { glob: '**/*.vue' } }, + { pattern: { glob: '**/*.md' } }, { pattern: { glob: '**/*.js' } }, { pattern: { glob: '**/*.ts' } }, { pattern: { glob: '**/*.jsx' } }, diff --git a/packages/vue-language-service/src/documentService.ts b/packages/vue-language-service/src/documentService.ts index b8b4e92b0..7e463c515 100644 --- a/packages/vue-language-service/src/documentService.ts +++ b/packages/vue-language-service/src/documentService.ts @@ -116,7 +116,7 @@ export function getDocumentService( function getVueDocument(document: TextDocument) { - if (document.languageId !== 'vue') + if (document.languageId !== 'vue' && document.languageId !== 'markdown') return; let vueDoc = vueDocuments.get(document); diff --git a/packages/vue-language-service/src/languageService.ts b/packages/vue-language-service/src/languageService.ts index de9d6459c..f69523dcc 100644 --- a/packages/vue-language-service/src/languageService.ts +++ b/packages/vue-language-service/src/languageService.ts @@ -327,7 +327,7 @@ export function createLanguageService( document = TextDocument.create( uri, - uri.endsWith('.vue') ? 'vue' : 'typescript', // TODO + shared.syntaxToLanguageId(upath.extname(uri).slice(1)), newVersion, scriptSnapshot.getText(0, scriptSnapshot.getLength()), ); diff --git a/packages/vue-language-service/src/plugins/vue-convert-htmlpug.ts b/packages/vue-language-service/src/plugins/vue-convert-htmlpug.ts index 7f4e245ab..542e53ec1 100644 --- a/packages/vue-language-service/src/plugins/vue-convert-htmlpug.ts +++ b/packages/vue-language-service/src/plugins/vue-convert-htmlpug.ts @@ -95,7 +95,7 @@ export default function (options: { function worker(uri: string, callback: (vueDocument: VueDocument) => T) { const vueDocument = options.getVueDocument(uri); - if (!vueDocument) + if (!vueDocument || vueDocument.file.fileName.endsWith('.md')) return; return callback(vueDocument); diff --git a/packages/vue-language-service/src/plugins/vue-convert-refsugar.ts b/packages/vue-language-service/src/plugins/vue-convert-refsugar.ts index 7c18effb6..ebe6e086d 100644 --- a/packages/vue-language-service/src/plugins/vue-convert-refsugar.ts +++ b/packages/vue-language-service/src/plugins/vue-convert-refsugar.ts @@ -49,19 +49,23 @@ export default function (options: { const result: vscode.CodeLens[] = []; const descriptor = vueDocument.file.getDescriptor(); const ranges = vueDocument.file.getSfcRefSugarRanges(); + const compiledVue = vueDocument.file.getCompiledVue()!; if (descriptor.scriptSetup && ranges) { - result.push({ - range: { - start: document.positionAt(descriptor.scriptSetup.startTagEnd), - end: document.positionAt(descriptor.scriptSetup.startTagEnd + descriptor.scriptSetup.content.length), - }, - command: { - title: 'ref sugar ' + (ranges.refs.length ? '☑' : '☐'), - command: ranges.refs.length ? Commands.UNUSE_REF_SUGAR : Commands.USE_REF_SUGAR, - arguments: [document.uri], - }, - }); + const startTagEnd = compiledVue.mapping({ start: descriptor.scriptSetup.startTagEnd, end: descriptor.scriptSetup.startTagEnd })?.start; + if (startTagEnd) { + result.push({ + range: { + start: document.positionAt(startTagEnd), + end: document.positionAt(startTagEnd + descriptor.scriptSetup.content.length), + }, + command: { + title: 'ref sugar ' + (ranges.refs.length ? '☑' : '☐'), + command: ranges.refs.length ? Commands.UNUSE_REF_SUGAR : Commands.USE_REF_SUGAR, + arguments: [document.uri], + }, + }); + } } return result; @@ -135,6 +139,12 @@ async function useRefSugar( _scriptSetupAst: NonNullable, ) { + const compiledVue = _vueDocument.file.getCompiledVue()!; + const startTagEnd = compiledVue.mapping({ start: _scriptSetup.startTagEnd, end: _scriptSetup.startTagEnd })?.start; + + if (startTagEnd === undefined) + return; + const ranges = parseDeclarationRanges(ts, _scriptSetupAst); const dotValueRanges = parseDotValueRanges(ts, _scriptSetupAst); const document = _vueDocument.getDocument(); @@ -146,7 +156,7 @@ async function useRefSugar( for (const binding of declaration.leftBindings) { - const definitions = await findTypeDefinition(document.uri, document.positionAt(_scriptSetup.startTagEnd + binding.end)) ?? []; + const definitions = await findTypeDefinition(document.uri, document.positionAt(startTagEnd + binding.end)) ?? []; const _isRefType = isRefType(definitions, scriptTsLs); if (!_isRefType) @@ -154,7 +164,7 @@ async function useRefSugar( isRefDeclaration = true; - let references = await findReferences(document.uri, document.positionAt(_scriptSetup.startTagEnd + binding.end)) ?? []; + let references = await findReferences(document.uri, document.positionAt(startTagEnd + binding.end)) ?? []; references = references.filter(reference => { @@ -164,13 +174,13 @@ async function useRefSugar( const start = document.offsetAt(reference.range.start); const end = document.offsetAt(reference.range.end); - if (start >= (_scriptSetup.startTagEnd + binding.start) && end <= (_scriptSetup.startTagEnd + binding.end)) + if (start >= (startTagEnd + binding.start) && end <= (startTagEnd + binding.end)) return false; - if (end < _scriptSetup.startTagEnd || start > _scriptSetup.startTagEnd + _scriptSetup.content.length) + if (end < startTagEnd || start > startTagEnd + _scriptSetup.content.length) return false; - if (isBlacklistNode(ts, _scriptSetupAst, start - _scriptSetup.startTagEnd)) + if (isBlacklistNode(ts, _scriptSetupAst, start - startTagEnd)) return false; return true; @@ -180,8 +190,8 @@ async function useRefSugar( const sfcStart = document.offsetAt(reference.range.start); const sfcEnd = document.offsetAt(reference.range.end); - const setupStart = sfcStart - _scriptSetup.startTagEnd; - const setupEnd = sfcEnd - _scriptSetup.startTagEnd; + const setupStart = sfcStart - startTagEnd; + const setupEnd = sfcEnd - startTagEnd; const dotValue = dotValueRanges.find(dot => dot.beforeDot === setupEnd); if (!dotValue) { @@ -224,8 +234,8 @@ async function useRefSugar( edits.push(vscode.TextEdit.replace( { - start: document.positionAt(_scriptSetup.startTagEnd + start), - end: document.positionAt(_scriptSetup.startTagEnd + end), + start: document.positionAt(startTagEnd! + start), + end: document.positionAt(startTagEnd! + end), }, text )); @@ -308,6 +318,12 @@ async function unuseRefSugar( _scriptSetupAst: NonNullable, ) { + const compiledVue = _vueDocument.file.getCompiledVue()!; + const startTagEnd = compiledVue.mapping({ start: _scriptSetup.startTagEnd, end: _scriptSetup.startTagEnd })?.start; + + if (startTagEnd === undefined) + return; + const ranges = _vueDocument.file.getSfcRefSugarRanges(); const document = _vueDocument.getDocument(); const edits: vscode.TextEdit[] = []; @@ -347,7 +363,7 @@ async function unuseRefSugar( await shared.sleep(0); const bindingName = _scriptSetup.content.substring(binding.start, binding.end); - const renames = await doRename(_vueDocument.uri, document.positionAt(_scriptSetup.startTagEnd + binding.end), bindingName + '.value'); + const renames = await doRename(_vueDocument.uri, document.positionAt(startTagEnd + binding.end), bindingName + '.value'); if (renames?.changes) { const edits_2 = renames.changes[_vueDocument.uri]; @@ -359,10 +375,10 @@ async function unuseRefSugar( end: document.offsetAt(edit.range.end), }; - if (editRange.start >= (_scriptSetup.startTagEnd + binding.start) && editRange.end <= (_scriptSetup.startTagEnd + binding.end)) + if (editRange.start >= (startTagEnd + binding.start) && editRange.end <= (startTagEnd + binding.end)) continue; - if (editRange.end < _scriptSetup.startTagEnd || editRange.start > _scriptSetup.startTagEnd + _scriptSetup.content.length) + if (editRange.end < startTagEnd || editRange.start > startTagEnd + _scriptSetup.content.length) continue; if (inRawCall(editRange.start, editRange.end)) @@ -385,7 +401,7 @@ async function unuseRefSugar( function inRawCall(start: number, end: number) { if (ranges) { for (const rawRange of ranges.raws) { - if (start >= (_scriptSetup.startTagEnd + rawRange.argsRange.start) && end <= (_scriptSetup.startTagEnd + rawRange.argsRange.end)) { + if (start >= (startTagEnd! + rawRange.argsRange.start) && end <= (startTagEnd! + rawRange.argsRange.end)) { return true; } } @@ -399,8 +415,8 @@ async function unuseRefSugar( edits.push(vscode.TextEdit.replace( { - start: document.positionAt(_scriptSetup.startTagEnd + start), - end: document.positionAt(_scriptSetup.startTagEnd + end), + start: document.positionAt(startTagEnd! + start), + end: document.positionAt(startTagEnd! + end), }, text )); diff --git a/packages/vue-language-service/src/plugins/vue-convert-scriptsetup.ts b/packages/vue-language-service/src/plugins/vue-convert-scriptsetup.ts index 03bdc9c59..153a0c2bf 100644 --- a/packages/vue-language-service/src/plugins/vue-convert-scriptsetup.ts +++ b/packages/vue-language-service/src/plugins/vue-convert-scriptsetup.ts @@ -38,32 +38,39 @@ export default function (options: { const result: vscode.CodeLens[] = []; const descriptor = vueDocument.file.getDescriptor(); + const compiledVue = vueDocument.file.getCompiledVue()!; if (descriptor.scriptSetup) { - result.push({ - range: { - start: document.positionAt(descriptor.scriptSetup.startTagEnd), - end: document.positionAt(descriptor.scriptSetup.startTagEnd + descriptor.scriptSetup.content.length), - }, - command: { - title: 'setup sugar ☑', - command: Commands.UNUSE_SETUP_SUGAR, - arguments: [document.uri], - }, - }); + const startTagEnd = compiledVue.mapping({ start: descriptor.scriptSetup.startTagEnd, end: descriptor.scriptSetup.startTagEnd })?.start; + if (startTagEnd) { + result.push({ + range: { + start: document.positionAt(startTagEnd), + end: document.positionAt(startTagEnd + descriptor.scriptSetup.content.length), + }, + command: { + title: 'setup sugar ☑', + command: Commands.UNUSE_SETUP_SUGAR, + arguments: [document.uri], + }, + }); + } } else if (descriptor.script) { - result.push({ - range: { - start: document.positionAt(descriptor.script.startTagEnd), - end: document.positionAt(descriptor.script.startTagEnd + descriptor.script.content.length), - }, - command: { - title: 'setup sugar ☐', - command: Commands.USE_SETUP_SUGAR, - arguments: [document.uri], - }, - }); + const startTagEnd = compiledVue.mapping({ start: descriptor.script.startTagEnd, end: descriptor.script.startTagEnd })?.start; + if (startTagEnd) { + result.push({ + range: { + start: document.positionAt(startTagEnd), + end: document.positionAt(startTagEnd + descriptor.script.content.length), + }, + command: { + title: 'setup sugar ☐', + command: Commands.USE_SETUP_SUGAR, + arguments: [document.uri], + }, + }); + } } return result; }); @@ -141,8 +148,14 @@ async function useSetupSugar( const ranges = parseUseScriptSetupRanges(ts, _scriptAst); const document = _vueDocument.getDocument(); + const compiledVue = _vueDocument.file.getCompiledVue()!; + const startTagEnd = compiledVue.mapping({ start: _script.startTagEnd, end: _script.startTagEnd })?.start; + + if (startTagEnd === undefined) + return; + const edits: vscode.TextEdit[] = []; - const scriptStartPos = document.positionAt(_script.startTagEnd); + const scriptStartPos = document.positionAt(startTagEnd); const startTagText = document.getText({ start: { line: scriptStartPos.line, @@ -261,8 +274,8 @@ async function useSetupSugar( function addReplace(start: number, end: number, text: string) { edits.push(vscode.TextEdit.replace( { - start: document.positionAt(_script.startTagEnd + start), - end: document.positionAt(_script.startTagEnd + end), + start: document.positionAt(startTagEnd! + start), + end: document.positionAt(startTagEnd! + end), }, text )); @@ -335,13 +348,18 @@ async function unuseSetupSugar( const ranges = parseUnuseScriptSetupRanges(ts, _scriptSetupAst); const scriptRanges = _scriptAst ? parseUseScriptSetupRanges(ts, _scriptAst) : undefined; + const compiledVue = _vueDocument.file.getCompiledVue()!; + const startTagEnd = compiledVue.mapping({ start: _scriptSetup.startTagEnd, end: _scriptSetup.startTagEnd })?.start; + + if (startTagEnd === undefined) + return; const document = _vueDocument.getDocument(); const edits: vscode.TextEdit[] = []; const removeSetupTextRanges: TextRange[] = [...ranges.imports]; const sfcCode = document.getText(); - const setupAttr = sfcCode.substring(0, _scriptSetup.startTagEnd).lastIndexOf(' setup'); + const setupAttr = sfcCode.substring(0, startTagEnd).lastIndexOf(' setup'); edits.push(vscode.TextEdit.replace( { @@ -554,8 +572,8 @@ async function unuseSetupSugar( edits.push(vscode.TextEdit.replace( { - start: document.positionAt(_scriptSetup.startTagEnd + start), - end: document.positionAt(_scriptSetup.startTagEnd + end), + start: document.positionAt(startTagEnd! + start), + end: document.positionAt(startTagEnd! + end), }, text )); diff --git a/packages/vue-language-service/src/plugins/vue-template.ts b/packages/vue-language-service/src/plugins/vue-template.ts index 6ab85e09b..103307d44 100644 --- a/packages/vue-language-service/src/plugins/vue-template.ts +++ b/packages/vue-language-service/src/plugins/vue-template.ts @@ -2,7 +2,6 @@ import * as shared from '@volar/shared'; import { parseScriptRanges } from '@volar/vue-code-gen/out/parsers/scriptRanges'; import { SearchTexts, TypeScriptRuntime, VueFile } from '@volar/vue-typescript'; import { VueDocument, VueDocuments } from '../vueDocuments'; -import { pauseTracking, resetTracking } from '@vue/reactivity'; import { camelize, capitalize, hyphenate } from '@vue/shared'; import { isIntrinsicElement } from '@volar/vue-code-gen'; import * as path from 'upath'; @@ -160,11 +159,11 @@ export default function useVueTemplateLanguagePlugin, - }; - const printText = printer.printNode(options.ts.EmitHint.Expression, newNode, scriptAst); - const editRange = vscode.Range.create( - textDoc.positionAt(descriptor.script.startTagEnd + exportDefault.componentsOption.start), - textDoc.positionAt(descriptor.script.startTagEnd + exportDefault.componentsOption.end), - ); - autoImportPositions.add(editRange.start); - autoImportPositions.add(editRange.end); - item.additionalTextEdits.push(vscode.TextEdit.replace( - editRange, - unescape(printText.replace(/\\u/g, '%u')), - )); - } - else if (exportDefault.args && exportDefault.argsNode) { - const newNode: typeof exportDefault.argsNode = { - ...exportDefault.argsNode, - properties: [ - ...exportDefault.argsNode.properties, - options.ts.factory.createShorthandPropertyAssignment(`components: { ${componentName} }`), - ] as any as ts.NodeArray, - }; - const printText = printer.printNode(options.ts.EmitHint.Expression, newNode, scriptAst); - const editRange = vscode.Range.create( - textDoc.positionAt(descriptor.script.startTagEnd + exportDefault.args.start), - textDoc.positionAt(descriptor.script.startTagEnd + exportDefault.args.end), - ); - autoImportPositions.add(editRange.start); - autoImportPositions.add(editRange.end); - item.additionalTextEdits.push(vscode.TextEdit.replace( - editRange, - unescape(printText.replace(/\\u/g, '%u')), - )); + const startTagEnd = compiledVue.mapping({ start: descriptor.script.startTagEnd, end: descriptor.script.startTagEnd })?.start; + if (startTagEnd !== undefined) { + const editPosition = textDoc.positionAt(startTagEnd + (scriptImport ? scriptImport.end : 0)); + autoImportPositions.add(editPosition); + item.additionalTextEdits = [ + vscode.TextEdit.insert( + editPosition, + '\n' + insertText, + ), + ]; + const scriptRanges = parseScriptRanges(options.ts, scriptAst, !!descriptor.scriptSetup, true, true); + const exportDefault = scriptRanges.exportDefault; + if (exportDefault) { + // https://github.com/microsoft/TypeScript/issues/36174 + const printer = options.ts.createPrinter(); + if (exportDefault.componentsOption && exportDefault.componentsOptionNode) { + const newNode: typeof exportDefault.componentsOptionNode = { + ...exportDefault.componentsOptionNode, + properties: [ + ...exportDefault.componentsOptionNode.properties, + options.ts.factory.createShorthandPropertyAssignment(componentName), + ] as any as ts.NodeArray, + }; + const printText = printer.printNode(options.ts.EmitHint.Expression, newNode, scriptAst); + const editRange = vscode.Range.create( + textDoc.positionAt(startTagEnd + exportDefault.componentsOption.start), + textDoc.positionAt(startTagEnd + exportDefault.componentsOption.end), + ); + autoImportPositions.add(editRange.start); + autoImportPositions.add(editRange.end); + item.additionalTextEdits.push(vscode.TextEdit.replace( + editRange, + unescape(printText.replace(/\\u/g, '%u')), + )); + } + else if (exportDefault.args && exportDefault.argsNode) { + const newNode: typeof exportDefault.argsNode = { + ...exportDefault.argsNode, + properties: [ + ...exportDefault.argsNode.properties, + options.ts.factory.createShorthandPropertyAssignment(`components: { ${componentName} }`), + ] as any as ts.NodeArray, + }; + const printText = printer.printNode(options.ts.EmitHint.Expression, newNode, scriptAst); + const editRange = vscode.Range.create( + textDoc.positionAt(startTagEnd + exportDefault.args.start), + textDoc.positionAt(startTagEnd + exportDefault.args.end), + ); + autoImportPositions.add(editRange.start); + autoImportPositions.add(editRange.end); + item.additionalTextEdits.push(vscode.TextEdit.replace( + editRange, + unescape(printText.replace(/\\u/g, '%u')), + )); + } } } } @@ -534,7 +540,10 @@ export default function useVueTemplateLanguagePlugin TextDocument.create(shared.fsPathToUri(vueFile.fileName), 'vue', documentVersion++, vueFile.refs.content.value)); + const document = computed(() => TextDocument.create(shared.fsPathToUri(vueFile.fileName), vueFile.fileName.endsWith('.md') ? 'markdown' : 'vue', documentVersion++, vueFile.refs.content.value)); const sourceMaps = computed(() => { return vueFile.refs.allEmbeddeds.value.map(embedded => sourceMapsMap.get(embedded)); }); @@ -259,13 +259,13 @@ export function parseVueDocument(vueFile: VueFile) { const offsets = tags.get(node.tag)!; const startTagHtmlOffset = node.loc.start.offset + node.loc.source.indexOf(node.tag); - const startTagTemplateOffset = htmlComputed.htmlToTemplate(startTagHtmlOffset, startTagHtmlOffset); + const startTagTemplateOffset = htmlComputed.mapping({ start: startTagHtmlOffset, end: startTagHtmlOffset }); if (startTagTemplateOffset !== undefined) { offsets.push(startTagTemplateOffset.start); } const endTagHtmlOffset = node.loc.start.offset + node.loc.source.lastIndexOf(node.tag); - const endTagTemplateOffset = htmlComputed.htmlToTemplate(endTagHtmlOffset, endTagHtmlOffset); + const endTagTemplateOffset = htmlComputed.mapping({ start: endTagHtmlOffset, end: endTagHtmlOffset }); if (endTagTemplateOffset !== undefined) { offsets.push(endTagTemplateOffset.end); } diff --git a/packages/vue-tsc/bin/vue-tsc.js b/packages/vue-tsc/bin/vue-tsc.js index 80ac8132c..5a5eeabd4 100755 --- a/packages/vue-tsc/bin/vue-tsc.js +++ b/packages/vue-tsc/bin/vue-tsc.js @@ -12,15 +12,15 @@ fs.readFileSync = (...args) => { // add *.vue files to allow extensions tsc = tsc.replace( `ts.supportedTSExtensions = [[".ts", ".tsx", ".d.ts"], [".cts", ".d.cts"], [".mts", ".d.mts"]];`, - `ts.supportedTSExtensions = [[".ts", ".tsx", ".d.ts"], [".cts", ".d.cts"], [".mts", ".d.mts"], [".vue"]];`, + `ts.supportedTSExtensions = [[".ts", ".tsx", ".d.ts"], [".cts", ".d.cts"], [".mts", ".d.mts"], [".vue", ".md"]];`, ); tsc = tsc.replace( `ts.supportedJSExtensions = [[".js", ".jsx"], [".mjs"], [".cjs"]];`, - `ts.supportedJSExtensions = [[".js", ".jsx"], [".mjs"], [".cjs"], [".vue"]];`, + `ts.supportedJSExtensions = [[".js", ".jsx"], [".mjs"], [".cjs"], [".vue", ".md"]];`, ); tsc = tsc.replace( `var allSupportedExtensions = [[".ts", ".tsx", ".d.ts", ".js", ".jsx"], [".cts", ".d.cts", ".cjs"], [".mts", ".d.mts", ".mjs"]];`, - `var allSupportedExtensions = [[".ts", ".tsx", ".d.ts", ".js", ".jsx"], [".cts", ".d.cts", ".cjs"], [".mts", ".d.mts", ".mjs"], [".vue"]];`, + `var allSupportedExtensions = [[".ts", ".tsx", ".d.ts", ".js", ".jsx"], [".cts", ".d.cts", ".cjs"], [".mts", ".d.mts", ".mjs"], [".vue", ".md"]];`, ); // proxy createProgram apis diff --git a/packages/vue-tsc/src/apis.ts b/packages/vue-tsc/src/apis.ts index 0838673a4..9036bf7b9 100644 --- a/packages/vue-tsc/src/apis.ts +++ b/packages/vue-tsc/src/apis.ts @@ -160,7 +160,7 @@ export function register( } } else { - file = ts.createSourceFile(fileName, docText, fileName.endsWith('.vue') ? ts.ScriptTarget.JSON : ts.ScriptTarget.Latest); + file = ts.createSourceFile(fileName, docText, fileName.endsWith('.vue') || fileName.endsWith('.md') ? ts.ScriptTarget.JSON : ts.ScriptTarget.Latest); } } const newDiagnostic: T = { diff --git a/packages/vue-typescript/package.json b/packages/vue-typescript/package.json index cefbba828..fd28f919a 100644 --- a/packages/vue-typescript/package.json +++ b/packages/vue-typescript/package.json @@ -13,6 +13,7 @@ "directory": "packages/vue-typescript" }, "devDependencies": { + "@types/markdown-it": "^12.2.3", "@volar/pug-language-service": "0.36.1", "typescript": "latest" }, @@ -21,9 +22,11 @@ "@volar/source-map": "0.36.1", "@volar/vue-code-gen": "0.36.1", "@vue/compiler-sfc": "^3.2.36", - "@vue/reactivity": "^3.2.36" + "@vue/reactivity": "^3.2.36", + "markdown-it": "^13.0.1", + "markdown-it-ast": "^0.0.1" }, "browser": { - "./out/plugins/pug.js": "./out/plugins/empty.js" + "./out/plugins/vue-template-pug.js": "./out/plugins/empty.js" } } diff --git a/packages/vue-typescript/src/plugins/file-md.ts b/packages/vue-typescript/src/plugins/file-md.ts new file mode 100644 index 000000000..c557e39e0 --- /dev/null +++ b/packages/vue-typescript/src/plugins/file-md.ts @@ -0,0 +1,192 @@ +import { VueLanguagePlugin } from '../vueFile'; +import * as MarkdownIt from 'markdown-it'; +// @ts-expect-error +import * as MarkdownItAst from 'markdown-it-ast'; +import { SourceMapBase, Mode } from '@volar/source-map'; +import { CodeGen } from '@volar/code-gen'; + +export default function (): VueLanguagePlugin { + + return { + + compileFileToVue(fileName, content) { + + if (fileName.endsWith('.md')) { + + let validTemplateBlock: [number, number] | undefined; + let validScriptBlock: [number, number] | undefined; + let validStyleBlock: [number, number] | undefined; + + const scriptLines: [number, number][] = []; + const styleLines: [number, number][] = []; + const templateLines: [number, number][] = []; + + const tokens = MarkdownIt().parse(content, {}); + const ast = MarkdownItAst.makeAST(tokens); + + for (const node of ast) { + // ') >= 0 + ) { + validScriptBlock[1] = node.children[0].map[1]; + scriptLines.push(validScriptBlock); + validScriptBlock = undefined; + continue; + } + if (validScriptBlock) { + continue; + } + // ') >= 0 + ) { + validStyleBlock[1] = node.children[0].map[1]; + styleLines.push(validStyleBlock); + validStyleBlock = undefined; + continue; + } + if (validStyleBlock) { + continue; + } + walkNode(node); + } + + breakTemplateBlock(); + + const codeGen = new CodeGen(); + const lines = content.split('\n'); + const lineOffsets: number[] = []; + let lineOffset = 0; + + for (const line of lines) { + lineOffsets.push(lineOffset); + lineOffset += line.length + 1; + } + + for (const _lines of scriptLines) { + const rangeLines = lines.slice(_lines[0], _lines[1]); + const rangeCode = rangeLines.join('\n'); + const start = lineOffsets[_lines[0]]; + codeGen.addCode( + rangeCode, + { + start: start, + end: start + rangeCode.length, + }, + Mode.Offset, + undefined, + ); + } + + for (const _lines of styleLines) { + const rangeLines = lines.slice(_lines[0], _lines[1]); + const rangeCode = rangeLines.join('\n'); + const start = lineOffsets[_lines[0]]; + codeGen.addCode( + rangeCode, + { + start: start, + end: start + rangeCode.length, + }, + Mode.Offset, + undefined, + ); + } + + if (templateLines.length) { + codeGen.addText('\n