Skip to content

Commit 7a46bac

Browse files
author
Dave Bartolomeo
committed
Save dirty documents before evaluating queries
1 parent 0451dd8 commit 7a46bac

10 files changed

Lines changed: 241 additions & 76 deletions

File tree

extensions/ql-vscode/package.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,8 @@
7171
"contributes": {
7272
"configurationDefaults": {
7373
"[ql]": {
74-
"editor.wordBasedSuggestions": false
74+
"editor.wordBasedSuggestions": false,
75+
"debug.saveBeforeStart": "nonUntitledEditorsInActiveGroup"
7576
},
7677
"[dbscheme]": {
7778
"editor.wordBasedSuggestions": false
@@ -246,8 +247,8 @@
246247
},
247248
"codeQL.runningQueries.autoSave": {
248249
"type": "boolean",
249-
"default": false,
250-
"description": "Enable automatically saving a modified query file when running a query."
250+
"description": "Enable automatically saving a modified query file when running a query.",
251+
"markdownDeprecationMessage": "This property is deprecated and no longer has any effect. To control automatic saving of documents before running queries, use the `debug.saveBeforeStart` setting."
251252
},
252253
"codeQL.runningQueries.maxQueries": {
253254
"type": "integer",

extensions/ql-vscode/src/config.ts

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
EventEmitter,
66
ConfigurationChangeEvent,
77
ConfigurationTarget,
8+
ConfigurationScope,
89
} from "vscode";
910
import { DistributionManager } from "./codeql-cli/distribution";
1011
import { extLogger } from "./common/logging/vscode";
@@ -23,7 +24,11 @@ export class Setting {
2324
parent?: Setting;
2425
private _hasChildren = false;
2526

26-
constructor(name: string, parent?: Setting) {
27+
constructor(
28+
name: string,
29+
parent?: Setting,
30+
private readonly languageId?: string,
31+
) {
2732
this.name = name;
2833
this.parent = parent;
2934
if (parent !== undefined) {
@@ -44,12 +49,22 @@ export class Setting {
4449
}
4550
}
4651

52+
get scope(): ConfigurationScope | undefined {
53+
if (this.languageId !== undefined) {
54+
return {
55+
languageId: this.languageId,
56+
};
57+
} else {
58+
return this.parent?.scope;
59+
}
60+
}
61+
4762
getValue<T>(): T {
4863
if (this.parent === undefined) {
4964
throw new Error("Cannot get the value of a root setting.");
5065
}
5166
return workspace
52-
.getConfiguration(this.parent.qualifiedName)
67+
.getConfiguration(this.parent.qualifiedName, this.parent.scope)
5368
.get<T>(this.name)!;
5469
}
5570

@@ -58,7 +73,7 @@ export class Setting {
5873
throw new Error("Cannot update the value of a root setting.");
5974
}
6075
return workspace
61-
.getConfiguration(this.parent.qualifiedName)
76+
.getConfiguration(this.parent.qualifiedName, this.parent.scope)
6277
.update(this.name, value, target);
6378
}
6479
}
@@ -69,6 +84,12 @@ export interface InspectionResult<T> {
6984
workspaceFolderValue?: T;
7085
}
7186

87+
const VSCODE_DEBUG_SETTING = new Setting("debug", undefined, "ql");
88+
export const VSCODE_SAVE_BEFORE_START_SETTING = new Setting(
89+
"saveBeforeStart",
90+
VSCODE_DEBUG_SETTING,
91+
);
92+
7293
const ROOT_SETTING = new Setting("codeQL");
7394

7495
// Global configuration
@@ -160,10 +181,6 @@ export const NUMBER_OF_TEST_THREADS_SETTING = new Setting(
160181
RUNNING_TESTS_SETTING,
161182
);
162183
export const MAX_QUERIES = new Setting("maxQueries", RUNNING_QUERIES_SETTING);
163-
export const AUTOSAVE_SETTING = new Setting(
164-
"autoSave",
165-
RUNNING_QUERIES_SETTING,
166-
);
167184
export const PAGE_SIZE = new Setting("pageSize", RESULTS_DISPLAY_SETTING);
168185
const CUSTOM_LOG_DIRECTORY_SETTING = new Setting(
169186
"customLogDirectory",

extensions/ql-vscode/src/debugger/debugger-ui.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { CoreQueryResults } from "../query-server";
1414
import {
1515
getQuickEvalContext,
1616
QueryOutputDir,
17+
saveBeforeStart,
1718
validateQueryUri,
1819
} from "../run-queries-shared";
1920
import { QLResolvedDebugConfiguration } from "./debug-configuration";
@@ -73,6 +74,9 @@ class QLDebugAdapterTracker
7374
}
7475

7576
public async quickEval(): Promise<void> {
77+
// Since we're not going through VS Code's launch path, we need to save dirty files ourselves.
78+
await saveBeforeStart();
79+
7680
const args: CodeQLProtocol.QuickEvalRequest["arguments"] = {
7781
quickEvalContext: await getQuickEvalContext(undefined, false),
7882
};

extensions/ql-vscode/src/local-queries/local-queries.ts

Lines changed: 2 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import {
1010
Range,
1111
Uri,
1212
window,
13-
workspace,
1413
} from "vscode";
1514
import {
1615
TeeLogger,
@@ -32,8 +31,8 @@ import {
3231
createInitialQueryInfo,
3332
createTimestampFile,
3433
getQuickEvalContext,
35-
promptUserToSaveChanges,
3634
QueryOutputDir,
35+
saveBeforeStart,
3736
SelectedQuery,
3837
validateQueryUri,
3938
} from "../run-queries-shared";
@@ -53,25 +52,6 @@ interface DatabaseQuickPickItem extends QuickPickItem {
5352
databaseItem: DatabaseItem;
5453
}
5554

56-
/**
57-
* If either the query file or the quickeval file is dirty, give the user the chance to save them.
58-
*/
59-
async function promptToSaveQueryIfNeeded(query: SelectedQuery): Promise<void> {
60-
// There seems to be no way to ask VS Code to find an existing text document by name, without
61-
// automatically opening the document if it is not found.
62-
const queryUri = Uri.file(query.queryPath).toString();
63-
const quickEvalUri =
64-
query.quickEval !== undefined
65-
? Uri.file(query.quickEval.quickEvalPosition.fileName).toString()
66-
: undefined;
67-
for (const openDocument of workspace.textDocuments) {
68-
const documentUri = openDocument.uri.toString();
69-
if (documentUri === queryUri || documentUri === quickEvalUri) {
70-
await promptUserToSaveChanges(openDocument);
71-
}
72-
}
73-
}
74-
7555
export enum QuickEvalType {
7656
None,
7757
QuickEval,
@@ -408,7 +388,7 @@ export class LocalQueries extends DisposableObject {
408388
const additionalPacks = getOnDiskWorkspaceFolders();
409389
const extensionPacks = await this.getDefaultExtensionPacks(additionalPacks);
410390

411-
await promptToSaveQueryIfNeeded(selectedQuery);
391+
await saveBeforeStart();
412392

413393
const coreQueryRun = this.queryRunner.createQueryRun(
414394
databaseItem.databaseUri.fsPath,

extensions/ql-vscode/src/run-queries-shared.ts

Lines changed: 66 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,14 @@ import * as legacyMessages from "./pure/legacy-messages";
33
import { DatabaseInfo, QueryMetadata } from "./common/interface-types";
44
import { join, parse, dirname, basename } from "path";
55
import {
6-
ConfigurationTarget,
76
Range,
87
TextDocument,
98
TextEditor,
109
Uri,
1110
window,
11+
workspace,
1212
} from "vscode";
13-
import { isCanary, AUTOSAVE_SETTING } from "./config";
14-
import { UserCancellationException } from "./common/vscode/progress";
13+
import { isCanary, VSCODE_SAVE_BEFORE_START_SETTING } from "./config";
1514
import {
1615
pathExists,
1716
readFile,
@@ -491,55 +490,78 @@ async function getSelectedPosition(
491490
};
492491
}
493492

493+
type SaveBeforeStartMode =
494+
| "nonUntitledEditorsInActiveGroup"
495+
| "allEditorsInActiveGroup"
496+
| "none";
497+
494498
/**
495-
* Prompts the user to save `document` if it has unsaved changes.
496-
*
497-
* @param document The document to save.
498-
*
499-
* @returns true if we should save changes and false if we should continue without saving changes.
500-
* @throws UserCancellationException if we should abort whatever operation triggered this prompt
499+
* Saves dirty files before running queries, based on the user's settings.
501500
*/
502-
export async function promptUserToSaveChanges(
503-
document: TextDocument,
504-
): Promise<boolean> {
505-
if (document.isDirty) {
506-
if (AUTOSAVE_SETTING.getValue()) {
507-
return true;
508-
} else {
509-
const yesItem = { title: "Yes", isCloseAffordance: false };
510-
const alwaysItem = { title: "Always Save", isCloseAffordance: false };
511-
const noItem = {
512-
title: "No (run version on disk)",
513-
isCloseAffordance: false,
514-
};
515-
const cancelItem = { title: "Cancel", isCloseAffordance: true };
516-
const message = `Query file '${basename(
517-
document.uri.fsPath,
518-
)}' has unsaved changes. Save now?`;
519-
const chosenItem = await window.showInformationMessage(
520-
message,
521-
{ modal: true },
522-
yesItem,
523-
alwaysItem,
524-
noItem,
525-
cancelItem,
526-
);
501+
export async function saveBeforeStart(): Promise<void> {
502+
const mode: SaveBeforeStartMode =
503+
(VSCODE_SAVE_BEFORE_START_SETTING.getValue<string>() as SaveBeforeStartMode) ??
504+
"nonUntitledEditorsInActiveGroup";
527505

528-
if (chosenItem === alwaysItem) {
529-
await AUTOSAVE_SETTING.updateValue(true, ConfigurationTarget.Workspace);
530-
return true;
531-
}
506+
switch (mode) {
507+
case "nonUntitledEditorsInActiveGroup":
508+
await saveAllInGroup(false);
509+
break;
532510

533-
if (chosenItem === yesItem) {
534-
return true;
535-
}
511+
case "allEditorsInActiveGroup":
512+
await saveAllInGroup(true);
513+
break;
536514

537-
if (chosenItem === cancelItem) {
538-
throw new UserCancellationException("Query run cancelled.", true);
515+
case "none":
516+
break;
517+
518+
default:
519+
// Unexpected value. Fall back to the default behavior.
520+
await saveAllInGroup(false);
521+
break;
522+
}
523+
}
524+
525+
// Used in tests
526+
export async function saveAllInGroup(includeUntitled: boolean): Promise<void> {
527+
// There's no good way to get from a `Tab` to a `TextDocument`, so we'll collect all of the dirty
528+
// documents indexed by their URI, and then compare those URIs against the URIs of the tabs.
529+
const dirtyDocumentUris = new Map<string, TextDocument>();
530+
for (const openDocument of workspace.textDocuments) {
531+
if (openDocument.isDirty) {
532+
console.warn(`${openDocument.uri.toString()} is dirty.`);
533+
if (!openDocument.isUntitled || includeUntitled) {
534+
dirtyDocumentUris.set(openDocument.uri.toString(), openDocument);
535+
}
536+
}
537+
}
538+
if (dirtyDocumentUris.size > 0) {
539+
console.warn(`${window.tabGroups.all.length} tab groups open`);
540+
console.warn(`${workspace.textDocuments.length} documents open`);
541+
const tabGroup = window.tabGroups.activeTabGroup;
542+
console.warn(`${tabGroup.tabs.length} tabs open in active group`);
543+
for (const tab of tabGroup.tabs) {
544+
const input = tab.input;
545+
// The `input` property can be of an arbitrary type, depending on the underlying tab type. For
546+
// text editors (and potentially others), it's an object with a `uri` property. That's all we
547+
// need to know to match it up with a dirty document.
548+
if (typeof input === "object") {
549+
const uri = (input as any).uri;
550+
if (uri instanceof Uri) {
551+
const document = dirtyDocumentUris.get(uri.toString());
552+
if (document !== undefined) {
553+
console.warn(`Saving ${uri.toString()}`);
554+
await document.save();
555+
// Remove the URI from the dirty list so we don't wind up saving the same file twice
556+
// if it's open in multiple editors.
557+
dirtyDocumentUris.delete(uri.toString());
558+
} else {
559+
console.warn(`Can't find ${uri.toString()}`);
560+
}
561+
}
539562
}
540563
}
541564
}
542-
return false;
543565
}
544566

545567
/**
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
select "clean"
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
testtesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttestselect "dirty"
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
select "other-clean"
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
testtesttesttesttesttestselect "other-dirty"

0 commit comments

Comments
 (0)