Skip to content
This repository was archived by the owner on Oct 15, 2024. It is now read-only.

Commit 00a8955

Browse files
committed
feat: add qmlformat support
Fix #263
1 parent e86cb12 commit 00a8955

File tree

6 files changed

+129
-32
lines changed

6 files changed

+129
-32
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ All features support multi-root workspace project.
1919
- Support `.qmllint.ini` configuration file
2020
- Code completion (requires PySide6 >= 6.4)
2121
- Preview QML file in a separate window (requires PySide6)
22+
- Format QML file (requires PySide6 >= 6.5.2)
2223

2324
### Qt UI Files
2425

package.json

Lines changed: 1 addition & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -143,11 +143,6 @@
143143
"command": "qtForPython.compileTranslations",
144144
"title": "Compile Qt Translation File (lrelease)",
145145
"category": "Qt for Python"
146-
},
147-
{
148-
"command": "qtForPython.formatQml",
149-
"title": "Format QML File (qmlformat)",
150-
"category": "Qt for Python"
151146
}
152147
],
153148
"menus": {
@@ -191,11 +186,6 @@
191186
"command": "qtForPython.compileTranslations",
192187
"when": "resourceExtname == .ts && resourceLangId == xml",
193188
"group": "qtForPython"
194-
},
195-
{
196-
"command": "qtForPython.formatQml",
197-
"when": "resourceLangId == qml",
198-
"group": "qtForPython"
199189
}
200190
],
201191
"explorer/context": [
@@ -238,11 +228,6 @@
238228
"command": "qtForPython.compileTranslations",
239229
"when": "resourceExtname == .ts && resourceLangId == xml",
240230
"group": "qtForPython"
241-
},
242-
{
243-
"command": "qtForPython.formatQml",
244-
"when": "resourceLangId == qml",
245-
"group": "qtForPython"
246231
}
247232
],
248233
"editor/title": [
@@ -285,11 +270,6 @@
285270
"command": "qtForPython.compileTranslations",
286271
"when": "resourceExtname == .ts && resourceLangId == xml",
287272
"group": "qtForPython"
288-
},
289-
{
290-
"command": "qtForPython.formatQml",
291-
"when": "resourceLangId == qml",
292-
"group": "qtForPython"
293273
}
294274
],
295275
"editor/context": [
@@ -332,11 +312,6 @@
332312
"command": "qtForPython.compileTranslations",
333313
"when": "resourceExtname == .ts && resourceLangId == xml",
334314
"group": "qtForPython"
335-
},
336-
{
337-
"command": "qtForPython.formatQml",
338-
"when": "resourceLangId == qml",
339-
"group": "qtForPython"
340315
}
341316
]
342317
},
@@ -512,9 +487,7 @@
512487
"items": {
513488
"type": "string"
514489
},
515-
"default": [
516-
"--inplace"
517-
],
490+
"default": [],
518491
"markdownDescription": "The options passed to `qmlformat` executable for QML formatting. See [here](https://github.com/seanwu1105/vscode-qt-for-python#predefined-variables) for a detailed list of predefined variables.",
519492
"scope": "resource"
520493
}

src/extension.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { catchError, of } from 'rxjs'
33
import type { ExtensionContext, OutputChannel } from 'vscode'
44
import { window } from 'vscode'
55
import { registerCommands$ } from './commands'
6+
import { registerQmlFormatter$ } from './qmlformat/format-qml'
67
import { registerQmlLanguageServer$ } from './qmlls/client'
78
import { registerQssColorProvider } from './qss/color-provider'
89
import { registerRccLiveExecution$ } from './rcc/rcc-live-execution'
@@ -27,6 +28,7 @@ export async function activate({
2728
registerUicLiveExecution$({ extensionUri }),
2829
registerRccLiveExecution$({ extensionUri }),
2930
registerQmlLanguageServer$({ extensionUri, outputChannel }),
31+
registerQmlFormatter$({ extensionUri }),
3032
]
3133

3234
const observer: Partial<Observer<Result>> = {

src/qmlformat/format-qml.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { ReplaySubject, firstValueFrom, using } from 'rxjs'
2+
import { Range, TextEdit, languages } from 'vscode'
3+
import type { URI } from 'vscode-uri'
4+
import type { ExecError, StdErrError } from '../run'
5+
import { run } from '../run'
6+
import { getToolCommand$ } from '../tool-utils'
7+
import type { SuccessResult } from '../types'
8+
import { type ErrorResult } from '../types'
9+
10+
export function registerQmlFormatter$({
11+
extensionUri,
12+
}: {
13+
readonly extensionUri: URI
14+
}) {
15+
const formatResult$ = new ReplaySubject<
16+
SuccessResult<string> | ErrorResult<'NotFound'> | ExecError | StdErrError
17+
>(1)
18+
19+
return using(
20+
() => {
21+
const disposable = languages.registerDocumentFormattingEditProvider(
22+
'qml',
23+
{
24+
async provideDocumentFormattingEdits(document) {
25+
const getToolCommandResult = await firstValueFrom(
26+
getToolCommand$({
27+
tool: 'qmlformat',
28+
extensionUri,
29+
resource: document.uri,
30+
}),
31+
)
32+
if (getToolCommandResult.kind !== 'Success') {
33+
formatResult$.next(getToolCommandResult)
34+
return []
35+
}
36+
37+
const { command, options } = getToolCommandResult.value
38+
const runResult = await run({
39+
command: [...command, ...options, document.uri.fsPath],
40+
})
41+
if (runResult.kind !== 'Success') {
42+
formatResult$.next(runResult)
43+
return []
44+
}
45+
46+
const formatted = runResult.value.stdout
47+
const fullRange = document.validateRange(
48+
new Range(
49+
document.lineAt(0).range.start,
50+
document.lineAt(document.lineCount - 1).range.end,
51+
),
52+
)
53+
formatResult$.next({
54+
kind: 'Success',
55+
value: `Formatted ${document.uri.fsPath}`,
56+
})
57+
return [TextEdit.replace(fullRange, formatted)]
58+
},
59+
},
60+
)
61+
return { unsubscribe: () => disposable.dispose() }
62+
},
63+
() => formatResult$.asObservable(),
64+
)
65+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import * as assert from 'node:assert'
2+
import * as path from 'node:path'
3+
import type { TextDocument, TextEdit } from 'vscode'
4+
import { WorkspaceEdit, commands, window, workspace } from 'vscode'
5+
import { URI } from 'vscode-uri'
6+
import {
7+
E2E_TIMEOUT,
8+
TEST_ASSETS_PATH,
9+
setupE2EEnvironment,
10+
} from '../test-utils'
11+
12+
suite('format-qml/e2e', () => {
13+
suiteSetup(async function () {
14+
this.timeout(E2E_TIMEOUT)
15+
await setupE2EEnvironment()
16+
})
17+
18+
suite('when a qml file is open', () => {
19+
const sampleFilenameNoExt = 'unformatted'
20+
let document: TextDocument
21+
22+
setup(async function () {
23+
this.timeout(E2E_TIMEOUT)
24+
25+
document = await workspace.openTextDocument(
26+
URI.file(
27+
path.resolve(TEST_ASSETS_PATH, 'qml', `${sampleFilenameNoExt}.qml`),
28+
),
29+
)
30+
await window.showTextDocument(document)
31+
})
32+
33+
teardown(async function () {
34+
this.timeout(E2E_TIMEOUT)
35+
await commands.executeCommand('workbench.action.closeActiveEditor')
36+
})
37+
38+
test('should be able to run formatQml command', async () => {
39+
const originalContent = document.getText()
40+
const edits: TextEdit[] = await commands.executeCommand(
41+
'vscode.executeFormatDocumentProvider',
42+
document.uri,
43+
)
44+
45+
const workspaceEdit = new WorkspaceEdit()
46+
workspaceEdit.set(document.uri, edits)
47+
await workspace.applyEdit(workspaceEdit)
48+
49+
return assert.notDeepStrictEqual(originalContent, document.getText())
50+
}).timeout(E2E_TIMEOUT)
51+
})
52+
}).timeout(E2E_TIMEOUT)

src/test/suite/test-utils.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import * as assert from 'node:assert'
22
import * as path from 'node:path'
33
import { extensions, workspace } from 'vscode'
44
import { URI } from 'vscode-uri'
5-
import { notNil } from '../../utils'
5+
import { isNil, notNil } from '../../utils'
66

77
// eslint-disable-next-line @typescript-eslint/no-var-requires
88
const { name, publisher } = require('../../../package.json')
@@ -55,16 +55,20 @@ export async function waitFor<T>(
5555
}
5656

5757
const start = Date.now()
58+
let error: unknown | undefined
5859
while (Date.now() - start < (options?.timeout ?? defaultOptions.timeout)) {
5960
try {
6061
return await callback()
6162
} catch (e) {
63+
error = e
6264
await sleep(options?.interval ?? defaultOptions.interval)
6365
}
6466
}
65-
throw new Error(
66-
`Timeout during waitFor: ${options?.timeout ?? defaultOptions.timeout}ms`,
67-
)
67+
if (isNil(error))
68+
throw new Error(
69+
`Timeout during waitFor: ${options?.timeout ?? defaultOptions.timeout}ms`,
70+
)
71+
throw error
6872
}
6973

7074
export async function forceDeleteFile(filename: string) {

0 commit comments

Comments
 (0)