diff --git a/package.json b/package.json index 6be6b85..24e19f2 100644 --- a/package.json +++ b/package.json @@ -84,13 +84,14 @@ }, "configuration": { "type": "object", - "title": "vscode-smithy configuration", + "title": "Smithy", "properties": { "smithyLsp.maxNumberOfProblems": { "scope": "resource", "type": "number", "default": 100, - "description": "Controls the maximum number of problems produced by the server." + "description": "Controls the maximum number of problems produced by the server.", + "deprecationMessage": "Use smithy.maxNumberOfProblems instead." }, "smithyLsp.trace.server": { "scope": "window", @@ -101,17 +102,15 @@ "verbose" ], "default": "verbose", - "description": "Traces the communication between VS Code and the language server." + "description": "Traces the communication between VS Code and the language server.", + "deprecationMessage": "Use smithy.trace.server instead." }, "smithyLsp.version": { "scope": "window", "type": "string", - "default": "0.6.0", - "description": "Version of the Smithy Language Server (see https://github.com/smithy-lang/smithy-language-server)." - }, - "smithyLsp.rootPath": { - "scope": "resource", - "type": "string" + "default": null, + "description": "Version of the Smithy Language Server (see https://github.com/smithy-lang/smithy-language-server).", + "deprecationMessage": "Use smithy.server.version instead." }, "smithyLsp.diagnostics.minimumSeverity": { "scope": "window", @@ -122,14 +121,53 @@ "DANGER", "ERROR" ], - "default": "WARNING", - "description": "Minimum severity of Smithy validation events to display in the editor." + "default": null, + "description": "Minimum severity of Smithy validation events to display in the editor.", + "deprecationMessage": "Use smithy.server.diagnostics.minimumSeverity instead." }, "smithyLsp.onlyReloadOnSave": { "scope": "window", "type": "boolean", "default": false, - "description": "Whether to only re-load the Smithy model on save. Use this if the server feels slow as you type." + "description": "Whether to only re-load the Smithy model on save. Use this if the server feels slow as you type.", + "deprecationMessage": "May cause features like definition, hover, and completions to behave incorrectly when you have unsaved changes." + }, + "smithy.maxNumberOfProblems": { + "scope": "resource", + "type": "number", + "default": 100, + "description": "Controls the maximum number of problems produced by the server." + }, + "smithy.trace.server": { + "type": "string", + "enum": [ + "off", + "messages", + "verbose" + ], + "default": "verbose", + "description": "Traces the communication between VS Code and the language server." + }, + "smithy.server.executable": { + "type": "string", + "default": null, + "description": "Executable to run the Smithy Language Server. Can be the executable name if it is on your PATH, or an absolute path to the executable." + }, + "smithy.server.version": { + "type": "string", + "default": "0.6.0", + "description": "Version of the Smithy Language Server to use. Ignored if smithy.server.executable is provided." + }, + "smithy.server.diagnostics.minimumSeverity": { + "type": "string", + "enum": [ + "NOTE", + "WARNING", + "DANGER", + "ERROR" + ], + "default": "WARNING", + "description": "Minimum severity of Smithy validation events to display in the editor." } } } diff --git a/src/config.ts b/src/config.ts new file mode 100644 index 0000000..8fa6a81 --- /dev/null +++ b/src/config.ts @@ -0,0 +1,29 @@ +import * as vscode from 'vscode'; + +export function getServerDiagnosticsMinimumSeverity(): string | undefined { + return getOldOrNewConfig('diagnostics.minimumSeverity', 'server.diagnostics.minimumSeverity'); +} + +export function getServerOnlyReloadOnSave(): boolean | undefined { + return getOldConfig('onlyReloadOnSave'); +} + +export function getServerExecutable(): string | undefined { + return getConfig('server.executable'); +} + +export function getServerVersion(): string { + return getOldOrNewConfig('version', 'server.version'); +} + +function getOldOrNewConfig(oldKey: string, newKey: string): T | undefined { + return getOldConfig(oldKey) || getConfig(newKey); +} + +function getConfig(key: string): T | undefined { + return vscode.workspace.getConfiguration('smithy').get(key); +} + +function getOldConfig(key: string): T | undefined { + return vscode.workspace.getConfiguration('smithyLsp').get(key); +} diff --git a/src/coursier/coursier.ts b/src/coursier/coursier.ts index 91c6aef..84fb574 100644 --- a/src/coursier/coursier.ts +++ b/src/coursier/coursier.ts @@ -1,12 +1,22 @@ -import { downloadCoursierIfRequired } from './download-coursier'; -import { findCoursierOnPath } from './path-check'; +import * as child_process from 'child_process'; +import * as vscode from 'vscode'; +import downloadCoursierIfRequired from './download-coursier'; -export function getCoursierExecutable(extensionPath: string): Promise { - return findCoursierOnPath(extensionPath).then((paths) => { - if (paths.length > 0) { - return paths[0]; - } else { - return downloadCoursierIfRequired(extensionPath, 'v2.0.6'); +export default async function getCoursierExecutable(context: vscode.ExtensionContext): Promise { + for (const command of ['cs', 'coursier']) { + if (await availableOnPath(command, ['--help'])) { + return command; } + } + + console.log('Coursier not found on path, downloading it instead.'); + return await downloadCoursierIfRequired(context.globalStoragePath, 'v2.0.6'); +} + +function availableOnPath(command: string, args: string[]): Promise { + return new Promise((resolve, reject) => { + child_process.execFile(command, args, (e, _, __) => { + resolve(e == null); + }); }); } diff --git a/src/coursier/download-coursier.ts b/src/coursier/download-coursier.ts index d88df59..b127ffa 100644 --- a/src/coursier/download-coursier.ts +++ b/src/coursier/download-coursier.ts @@ -4,7 +4,7 @@ import { IncomingMessage } from 'http'; import * as fs from 'fs'; import { access, mkdir } from 'fs/promises'; -export function downloadCoursierIfRequired(extensionPath: string, versionPath: string): Promise { +export default function downloadCoursierIfRequired(extensionPath: string, versionPath: string): Promise { function binPath(filename: string) { return path.join(extensionPath, filename); } diff --git a/src/coursier/path-check.ts b/src/coursier/path-check.ts deleted file mode 100644 index 54a1142..0000000 --- a/src/coursier/path-check.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { spawn } from 'child_process'; - -/** - * This type is used to bypass the `defaultImpl` when running the tests. - */ -export type ExecForCode = { - run: (execName: string, args: Array, cwd: string) => Promise; -}; - -const defaultImpl: ExecForCode = { - run: (execName: string, args: Array, cwd: string) => { - return new Promise((resolve, reject) => { - const options = { cwd }; - const resolveProcess = spawn(execName, args, options); - resolveProcess.on('exit', (exitCode) => { - resolve(exitCode); - }); - resolveProcess.on('error', (err) => { - reject(err); - }); - }); - }, -}; - -export function findCoursierOnPath(cwd: string, execForCode: ExecForCode = defaultImpl): Promise> { - function availableOnPath(execName: string): Promise { - return execForCode - .run(execName, ['--help'], cwd) - .then((ec) => ec === 0) - .catch(() => false); - } - - const possibleCoursierNames = ['cs', 'coursier']; - return possibleCoursierNames.reduce((accP, current) => { - return accP.then((acc) => { - return availableOnPath(current).then((succeeeded) => { - return succeeeded ? [...acc, current] : acc; - }); - }); - }, Promise.resolve([])); -} diff --git a/src/extension.ts b/src/extension.ts index 7ee69d0..0195095 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,268 +1,101 @@ -import * as net from 'net'; -import * as fs from 'fs'; -import * as child_process from 'child_process'; import * as vscode from 'vscode'; -import { SelectorDecorator } from './selector/selector-decorator'; -import { selectorRunCommandHandler, selectorClearCommandHandler } from './selector/selector-command-handlers'; +import * as lsp from 'vscode-languageclient/node'; -import { - CancellationToken, - DocumentFormattingRequest, - LanguageClient, - LanguageClientOptions, - RequestType, - RevealOutputChannelOn, - StreamInfo, - TextDocumentIdentifier, -} from 'vscode-languageclient/node'; -import { getCoursierExecutable } from './coursier/coursier'; +import * as config from './config'; +import JarFileContentsProvider from './jar-file-contents'; +import SelectorHandler from './selector'; +import getCoursierExecutable from './coursier/coursier'; -// Couriser uses an index to determine where to download jvms from: https://get-coursier.io/docs/2.0.6/cli-java#jvm-index -// Newer versions of coursier use this index, which is more up to date than the one -// used by the coursier version used by the extension. -// This is a temporary solution to avoid adding logic that determines the version of -// coursier on the local machine. In the near future, we will vend the language server -// as a standalone executable, and will no longer need couriser to manage the jvm version. -const COURSIER_JVM_INDEX = 'https://raw.githubusercontent.com/coursier/jvm-index/master/index.json'; - -let client: LanguageClient; - -export function activate(context: vscode.ExtensionContext) { - async function createServer(): Promise { - function startServer(executable: string): Promise { - console.log(`Executable located at ${executable}.`); - return new Promise((resolve, reject) => { - const server = net - .createServer((socket) => { - console.log('Creating server'); - - resolve({ - reader: socket, - writer: socket, - }); - - socket.on('end', () => console.log('Disconnected')); - }) - .on('error', (err) => { - // handle errors here - reject(err); - }); - - // grab a random port. - server.listen(() => { - // Start the child java process - let options = { cwd: context.extensionPath }; - - let port = (server.address() as net.AddressInfo).port; - - let version = vscode.workspace.getConfiguration('smithyLsp').get('version', '`'); - - // Downloading latest poms - let resolveArgs = [ - 'resolve', - '--mode', - 'force', - 'software.amazon.smithy:smithy-language-server:' + version, - '-r', - 'm2local', - ]; - let resolveProcess = child_process.spawn(executable, resolveArgs, options); - console.log(resolveArgs); - resolveProcess.on('exit', (exitCode) => { - console.log('Exit code : ' + exitCode); - if (exitCode == 0) { - console.log('Launching smithy-language-server version:' + version); - - let launchargs = [ - 'launch', - 'software.amazon.smithy:smithy-language-server:' + version, - // Configure couriser to use java 21 - '--jvm', - // By default, coursier uses AdoptOpenJDK: https://get-coursier.io/docs/2.0.6/cli-java - // We could just say '21' here, and let coursier default to adopt jdk - // 21, but later versions of the jdk are released under the name adoptium. - 'corretto:21', - // The location to download the jvm from is provided by the jvm index. - '--jvm-index', - COURSIER_JVM_INDEX, - '-r', - 'm2local', - '-M', - 'software.amazon.smithy.lsp.Main', - '--', - port.toString(), - ]; - - console.log(launchargs); - - let childProcess = child_process.spawn(executable, launchargs, options); +let client: lsp.LanguageClient; - childProcess.stdout.on('data', (data) => { - console.log(`stdout: ${data}`); - }); - - childProcess.stderr.on('data', (data) => { - console.error(`stderr: ${data}`); - }); - - childProcess.on('close', (code) => { - console.log(`LSP exited with code ${code}`); - }); - } else { - console.log(`Could not resolve smithy-language-server implementation`); - } - }); - - // Send raw output to a file - if (context.storageUri) { - if (!fs.existsSync(context.storageUri.fsPath)) { - fs.mkdirSync(context.storageUri.fsPath); - } - } - }); - }); - } - - const binaryPath = await getCoursierExecutable(context.globalStoragePath); - return await startServer(binaryPath); - } +export async function activate(context: vscode.ExtensionContext) { + const server = await getServer(context); + const clientOptions = getClientOptions(); // Create the language client and start the client. - client = new LanguageClient('smithyLsp', 'Smithy LSP', createServer, getClientOptions()); - - // Set client on `this` context to use with command handlers. - this.client = client; - - const smithyContentProvider = createSmithyContentProvider(client); - context.subscriptions.push( - vscode.workspace.registerTextDocumentContentProvider('smithyjar', smithyContentProvider) - ); + client = new lsp.LanguageClient('smithy', 'Smithy', server, clientOptions); - const smithyFormattingEditProvider = createSmithyFormattingEditProvider(client); - context.subscriptions.push( - vscode.languages.registerDocumentFormattingEditProvider('smithy', smithyFormattingEditProvider) - ); - - // Set default expression input, and use context to hold state between command invocations. - this.expression = 'Enter Selector Expression'; - this.selectorDecorator = new SelectorDecorator(); + const jarFileContentsProvider = new JarFileContentsProvider(client); + const selectorHandler = new SelectorHandler(client); - // Register selector commands. - context.subscriptions.push(vscode.commands.registerCommand('smithy.runSelector', selectorRunCommandHandler, this)); context.subscriptions.push( - vscode.commands.registerCommand('smithy.clearSelector', selectorClearCommandHandler, this) + vscode.workspace.registerTextDocumentContentProvider('smithyjar', jarFileContentsProvider), + vscode.commands.registerCommand('smithy.runSelector', selectorHandler.run, selectorHandler), + vscode.commands.registerCommand('smithy.clearSelector', selectorHandler.clear, selectorHandler) ); // Start the client. This will also launch the server client.start(); } -function getClientOptions(): LanguageClientOptions { - let workspaceFolder: vscode.WorkspaceFolder; - - let rootPath: string = vscode.workspace.getConfiguration('smithyLsp').get('rootPath'); - - if (rootPath) { - const workspaceRoot = getWorkspaceRoot(); - if (rootPath.startsWith('${workspaceRoot}') && workspaceRoot === '') { - console.warn(`Unable to retrieve the workspace root.`); - } - workspaceFolder = { - uri: vscode.Uri.file(rootPath.replace('${workspaceRoot}', workspaceRoot)), - name: 'smithy-lsp-root-path', - index: 1, - }; +export function deactivate(): Thenable | undefined { + if (!client) { + return undefined; } + return client.stop(); +} - // Configure file patterns relative to the workspace folder. - let filePattern: vscode.GlobPattern = '**/{smithy-build}.json'; - let selectorPattern: string = null; - if (workspaceFolder) { - filePattern = new vscode.RelativePattern(workspaceFolder, filePattern); - selectorPattern = `${workspaceFolder.uri.fsPath}/**/*`; - } +function getClientOptions(): lsp.LanguageClientOptions { + const initializationOptions = { + 'diagnostics.minimumSeverity': config.getServerDiagnosticsMinimumSeverity(), + onlyReloadOnSave: config.getServerOnlyReloadOnSave(), + }; - // Options to control the language client return { - // Register the server for plain text documents - documentSelector: [ - { scheme: 'file', language: 'smithy', pattern: selectorPattern }, - { scheme: 'smithyjar', language: 'smithy', pattern: selectorPattern }, - { scheme: 'file', language: 'json', pattern: '**/{smithy-build,.smithy-project}.json' }, - ], - synchronize: { - // Notify the server about file changes to 'smithy-build.json' files contained in the workspace - fileEvents: vscode.workspace.createFileSystemWatcher(filePattern), - }, outputChannelName: 'Smithy Language Server', - - workspaceFolder, - - initializationOptions: { - 'diagnostics.minimumSeverity': vscode.workspace - .getConfiguration('smithyLsp') - .get('diagnostics.minimumSeverity'), - onlyReloadOnSave: vscode.workspace.getConfiguration('smithyLsp').get('onlyReloadOnSave'), - }, - // Don't switch to output window when the server returns output. - revealOutputChannelOn: RevealOutputChannelOn.Never, + revealOutputChannelOn: lsp.RevealOutputChannelOn.Never, progressOnInitialization: true, - }; -} - -export function deactivate(): Thenable | undefined { - if (!client) { - return undefined; - } - return client.stop(); -} -function getWorkspaceRoot(): string | undefined { - let folders = vscode.workspace.workspaceFolders; - if (!folders || folders.length === 0) { - return ''; - } - let folder = folders[0]; - if (folder.uri.scheme === 'file') { - return folder.uri.fsPath; - } - return ''; -} + documentSelector: [ + { language: 'smithy' }, + { scheme: 'smithyjar' }, + { pattern: '**/{smithy-build,.smithy-project}.json' }, + ], -function createSmithyContentProvider(languageClient: LanguageClient): vscode.TextDocumentContentProvider { - return { - provideTextDocumentContent: async (uri: vscode.Uri, token: CancellationToken): Promise => { - return languageClient - .sendRequest(ClassFileContentsRequest.type, { uri: uri.toString() }, token) - .then((v: string): string => { - return v || ''; - }); - }, + initializationOptions, }; } -function createSmithyFormattingEditProvider(languageClient: LanguageClient): vscode.DocumentFormattingEditProvider { - return { - provideDocumentFormattingEdits: async ( - document: vscode.TextDocument, - options: vscode.FormattingOptions, - token: CancellationToken - ): Promise => { - document.uri; - return languageClient - .sendRequest( - DocumentFormattingRequest.type, - { textDocument: { uri: document.uri.toString() }, options: options }, - token - ) - .then((v: vscode.TextEdit[]): vscode.TextEdit[] => { - return v; - }); - }, - }; -} +// Couriser uses an index to determine where to download jvms from: https://get-coursier.io/docs/2.0.6/cli-java#jvm-index +// Newer versions of coursier use this index, which is more up to date than the one +// used by the coursier version used by the extension. +// This is a temporary solution to avoid adding logic that determines the version of +// coursier on the local machine. In the near future, we will vend the language server +// as a standalone executable, and will no longer need couriser to manage the jvm version. +const COURSIER_JVM_INDEX = 'https://raw.githubusercontent.com/coursier/jvm-index/master/index.json'; -export namespace ClassFileContentsRequest { - export const type = new RequestType('smithy/jarFileContents'); +async function getServer(context: vscode.ExtensionContext): Promise { + const serverExecutable = config.getServerExecutable(); + if (serverExecutable) { + return { + command: serverExecutable, + args: ['0'], + }; + } else { + const coursierExecutable = await getCoursierExecutable(context); + const languageServerVersion = config.getServerVersion(); + return { + command: coursierExecutable, + args: [ + 'launch', + 'software.amazon.smithy:smithy-language-server:' + languageServerVersion, + // Configure couriser to use java 21 + '--jvm', + // By default, coursier uses AdoptOpenJDK: https://get-coursier.io/docs/2.0.6/cli-java + // We could just say '21' here, and let coursier default to adopt jdk + // 21, but later versions of the jdk are released under the name adoptium. + 'corretto:21', + // The location to download the jvm from is provided by the jvm index. + '--jvm-index', + COURSIER_JVM_INDEX, + '-r', + 'm2local', + '-M', + 'software.amazon.smithy.lsp.Main', + '--', + '0', + ], + }; + } } diff --git a/src/jar-file-contents.ts b/src/jar-file-contents.ts new file mode 100644 index 0000000..3a0328a --- /dev/null +++ b/src/jar-file-contents.ts @@ -0,0 +1,24 @@ +import * as vscode from 'vscode'; +import * as lsp from 'vscode-languageclient/node'; + +namespace JarFileContentsRequest { + type Params = lsp.TextDocumentIdentifier; + + type Result = string; + + const method = 'smithy/jarFileContents'; + + export const type = new lsp.RequestType(method); +} + +export default class JarFileContentsProvider implements vscode.TextDocumentContentProvider { + private client: lsp.LanguageClient; + + constructor(client: lsp.LanguageClient) { + this.client = client; + } + + provideTextDocumentContent(uri: vscode.Uri, token: vscode.CancellationToken): vscode.ProviderResult { + return this.client.sendRequest(JarFileContentsRequest.type, { uri: uri.toString() }, token); + } +} diff --git a/src/selector.ts b/src/selector.ts new file mode 100644 index 0000000..54fe742 --- /dev/null +++ b/src/selector.ts @@ -0,0 +1,75 @@ +import * as vscode from 'vscode'; +import * as lsp from 'vscode-languageclient/node'; + +namespace SelectorCommandRequest { + type Params = { + expression: string; + }; + + type Result = lsp.Location[]; + + const method = 'smithy/selectorCommand'; + + export const type = new lsp.RequestType(method); +} + +export default class SelectorHandler { + private client: lsp.LanguageClient; + private expression: string = 'Enter selector expression'; + private decorationType: vscode.TextEditorDecorationType; + + constructor(client: lsp.LanguageClient) { + this.client = client; + this.decorationType = createDecorationType(); + } + + async run() { + const expression = await vscode.window.showInputBox({ + title: 'Run a selector', + value: this.expression, + }); + + // Don't do anything if expression was not populated. + if (!expression) { + return; + } + + // Don't do anything if there's no active editor. + const activeEditor = vscode.window.activeTextEditor; + if (!activeEditor) { + return; + } + + await this.clear(); + this.expression = expression; + + const response = await this.client.sendRequest(SelectorCommandRequest.type, { expression }); + + const ranges: vscode.Range[] = []; + for (const location of response) { + if (location.uri.endsWith(activeEditor.document.fileName)) { + const range = new vscode.Range( + location.range.start.line, + location.range.start.character, + location.range.end.line, + location.range.end.character + ); + ranges.push(range); + } + } + + activeEditor.setDecorations(this.decorationType, ranges); + } + + async clear() { + this.decorationType.dispose(); + this.decorationType = createDecorationType(); + } +} + +function createDecorationType(): vscode.TextEditorDecorationType { + return vscode.window.createTextEditorDecorationType({ + border: 'dotted', + borderColor: '#C44536', + }); +} diff --git a/src/selector/selector-command-handlers.ts b/src/selector/selector-command-handlers.ts deleted file mode 100644 index c158692..0000000 --- a/src/selector/selector-command-handlers.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { Position, Range, window } from 'vscode'; -import { SelectorCommandRequest } from './selector-command-request'; - -export async function selectorClearCommandHandler() { - this.selectorDecorator.clear(); -} - -export async function selectorRunCommandHandler() { - const expression = await window.showInputBox({ - title: 'Run a selector', - value: this.expression, - }); - const decorator = this.selectorDecorator; - let response = []; - // Don't do anything if expression was not populated. - if (expression) { - decorator.clear(); - this.expression = expression; - response = await this.client.sendRequest(SelectorCommandRequest.type, { expression: expression }); - const activeEditor = window.activeTextEditor; - const ranges = []; - for (const location of response) { - if (location['uri'].endsWith(activeEditor.document.fileName)) { - const startPosition = new Position( - location['range']['start']['line'], - location['range']['start']['character'] - ); - const endPosition = new Position( - location['range']['end']['line'], - location['range']['end']['character'] - ); - ranges.push(new Range(startPosition, endPosition)); - } - } - decorator.set(activeEditor, ranges); - } -} diff --git a/src/selector/selector-command-request.ts b/src/selector/selector-command-request.ts deleted file mode 100644 index 6f910f2..0000000 --- a/src/selector/selector-command-request.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { RequestType } from 'vscode-languageclient'; - -interface SelectorParams { - expression: String; -} - -export namespace SelectorCommandRequest { - export const type = new RequestType('smithy/selectorCommand'); -} diff --git a/src/selector/selector-decorator.ts b/src/selector/selector-decorator.ts deleted file mode 100644 index 80163cd..0000000 --- a/src/selector/selector-decorator.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { Range, TextEditor, TextEditorDecorationType, window, workspace } from 'vscode'; - -export interface ISelectorDecorator { - getDecorationType(): TextEditorDecorationType; - clear(): void; - set(textEditor: TextEditor, ranges: readonly Range[]): void; -} - -export class SelectorDecorator { - private decorationType: TextEditorDecorationType; - - constructor() { - this.decorationType = this.createDecorationType(); - } - - getDecorationType(): TextEditorDecorationType { - return this.decorationType; - } - - clear(): void { - this.decorationType.dispose(); - this.decorationType = this.createDecorationType(); - } - - createDecorationType(): TextEditorDecorationType { - return window.createTextEditorDecorationType({ - border: 'dotted', - borderColor: '#C44536', - }); - } - - set(textEditor: TextEditor, ranges: readonly Range[]): void { - textEditor.setDecorations(this.decorationType, ranges); - } -} diff --git a/tests/suite5/extension.test.ts b/tests/suite5/extension.test.ts index b28f181..e019c24 100644 --- a/tests/suite5/extension.test.ts +++ b/tests/suite5/extension.test.ts @@ -10,11 +10,10 @@ suite('formatting tests', function () { const doc = await vscode.workspace.openTextDocument(smithyMainUri); await vscode.window.showTextDocument(doc); await waitForServerStartup(); - const edits: vscode.TextEdit[] = await vscode.commands.executeCommand( - 'vscode.executeFormatDocumentProvider', - smithyMainUri - ); - assert.strictEqual(edits.length > 0, true, 'expected edits from formatter, but got none'); - return Promise.resolve(); + + const beforeEditText = doc.getText(); + await vscode.commands.executeCommand('editor.action.formatDocument', smithyMainUri); + const afterEditText = doc.getText(); + assert.notStrictEqual(afterEditText, beforeEditText); }); });