toggleWordWrap.ts 11.2 KB
Newer Older
I
ivanixgames 已提交
1 2 3 4 5
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

A
Alex Dima 已提交
6
import * as nls from 'vs/nls';
J
Johannes Rieken 已提交
7
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
A
Alex Dima 已提交
8
import { Disposable } from 'vs/base/common/lifecycle';
9
import { URI } from 'vs/base/common/uri';
10
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
A
Alex Dima 已提交
11 12
import { EditorAction, ServicesAccessor, registerEditorAction, registerEditorContribution } from 'vs/editor/browser/editorExtensions';
import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService';
A
renames  
Alex Dima 已提交
13
import { EditorOption, EditorOptions } from 'vs/editor/common/config/editorOptions';
A
Alex Dima 已提交
14 15 16 17 18
import { IEditorContribution } from 'vs/editor/common/editorCommon';
import { ITextModel } from 'vs/editor/common/model';
import { ITextResourceConfigurationService } from 'vs/editor/common/services/resourceConfiguration';
import { MenuId, MenuRegistry } from 'vs/platform/actions/common/actions';
import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
19
import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
A
Alex Dima 已提交
20
import { INotificationService } from 'vs/platform/notification/common/notification';
21
import { DefaultSettingsEditorContribution } from 'vs/workbench/contrib/preferences/browser/preferencesEditor';
22
import { registerAndGetAmdImageURL } from 'vs/base/common/amd';
A
Alex Dima 已提交
23

24 25 26 27 28 29 30 31 32 33 34 35 36 37
const transientWordWrapState = 'transientWordWrapState';
const isWordWrapMinifiedKey = 'isWordWrapMinified';
const isDominatedByLongLinesKey = 'isDominatedByLongLines';
const inDiffEditorKey = 'inDiffEditor';

/**
 * State written/read by the toggle word wrap action and associated with a particular model.
 */
interface IWordWrapTransientState {
	readonly forceWordWrap: 'on' | 'off' | 'wordWrapColumn' | 'bounded';
	readonly forceWordWrapMinified: boolean;
}

interface IWordWrapState {
A
Alex Dima 已提交
38
	readonly configuredWordWrap: 'on' | 'off' | 'wordWrapColumn' | 'bounded' | undefined;
39
	readonly configuredWordWrapMinified: boolean;
A
Alex Dima 已提交
40
	readonly transientState: IWordWrapTransientState | null;
41 42 43 44 45
}

/**
 * Store (in memory) the word wrap state for a particular model.
 */
46
export function writeTransientState(model: ITextModel, state: IWordWrapTransientState | null, codeEditorService: ICodeEditorService): void {
47 48 49 50 51 52
	codeEditorService.setTransientModelProperty(model, transientWordWrapState, state);
}

/**
 * Read (in memory) the word wrap state for a particular model.
 */
A
Alex Dima 已提交
53
function readTransientState(model: ITextModel, codeEditorService: ICodeEditorService): IWordWrapTransientState {
54 55 56
	return codeEditorService.getTransientModelProperty(model, transientWordWrapState);
}

A
Alex Dima 已提交
57
function readWordWrapState(model: ITextModel, configurationService: ITextResourceConfigurationService, codeEditorService: ICodeEditorService): IWordWrapState {
58
	const editorConfig = configurationService.getValue(model.uri, 'editor') as { wordWrap: 'on' | 'off' | 'wordWrapColumn' | 'bounded'; wordWrapMinified: boolean };
R
Rob Lourens 已提交
59
	let _configuredWordWrap = editorConfig && (typeof editorConfig.wordWrap === 'string' || typeof editorConfig.wordWrap === 'boolean') ? editorConfig.wordWrap : undefined;
A
Alex Dima 已提交
60 61 62 63 64 65 66 67

	// Compatibility with old true or false values
	if (<any>_configuredWordWrap === true) {
		_configuredWordWrap = 'on';
	} else if (<any>_configuredWordWrap === false) {
		_configuredWordWrap = 'off';
	}

R
Rob Lourens 已提交
68
	const _configuredWordWrapMinified = editorConfig && typeof editorConfig.wordWrapMinified === 'boolean' ? editorConfig.wordWrapMinified : undefined;
69 70
	const _transientState = readTransientState(model, codeEditorService);
	return {
A
Alex Dima 已提交
71
		configuredWordWrap: _configuredWordWrap,
A
renames  
Alex Dima 已提交
72
		configuredWordWrapMinified: (typeof _configuredWordWrapMinified === 'boolean' ? _configuredWordWrapMinified : EditorOptions.wordWrapMinified.defaultValue),
73 74 75 76
		transientState: _transientState
	};
}

77
function toggleWordWrap(editor: ICodeEditor, state: IWordWrapState): IWordWrapState {
78 79 80 81 82 83 84 85 86 87 88
	if (state.transientState) {
		// toggle off => go to null
		return {
			configuredWordWrap: state.configuredWordWrap,
			configuredWordWrapMinified: state.configuredWordWrapMinified,
			transientState: null
		};
	}

	let transientState: IWordWrapTransientState;

A
renames  
Alex Dima 已提交
89
	const actualWrappingInfo = editor.getOption(EditorOption.wrappingInfo);
90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116
	if (actualWrappingInfo.isWordWrapMinified) {
		// => wrapping due to minified file
		transientState = {
			forceWordWrap: 'off',
			forceWordWrapMinified: false
		};
	} else if (state.configuredWordWrap !== 'off') {
		// => wrapping is configured to be on (or some variant)
		transientState = {
			forceWordWrap: 'off',
			forceWordWrapMinified: false
		};
	} else {
		// => wrapping is configured to be off
		transientState = {
			forceWordWrap: 'on',
			forceWordWrapMinified: state.configuredWordWrapMinified
		};
	}

	return {
		configuredWordWrap: state.configuredWordWrap,
		configuredWordWrapMinified: state.configuredWordWrapMinified,
		transientState: transientState
	};
}

117
const TOGGLE_WORD_WRAP_ID = 'editor.action.toggleWordWrap';
A
Alex Dima 已提交
118
class ToggleWordWrapAction extends EditorAction {
A
Alex Dima 已提交
119 120

	constructor() {
121
		super({
122
			id: TOGGLE_WORD_WRAP_ID,
123 124
			label: nls.localize('toggle.wordwrap', "View: Toggle Word Wrap"),
			alias: 'View: Toggle Word Wrap',
125
			precondition: undefined,
126
			kbOpts: {
127
				kbExpr: null,
A
Alex Dima 已提交
128
				primary: KeyMod.Alt | KeyCode.KEY_Z,
129
				weight: KeybindingWeight.EditorContrib
130 131
			}
		});
I
ivanixgames 已提交
132 133
	}

134
	public run(accessor: ServicesAccessor, editor: ICodeEditor): void {
135 136 137 138
		if (editor.getContribution(DefaultSettingsEditorContribution.ID)) {
			// in the settings editor...
			return;
		}
A
Alex Dima 已提交
139 140 141
		if (!editor.hasModel()) {
			return;
		}
A
renames  
Alex Dima 已提交
142
		if (editor.getOption(EditorOption.inDiffEditor)) {
A
Alex Dima 已提交
143
			// Cannot change wrapping settings inside the diff editor
144 145
			const notificationService = accessor.get(INotificationService);
			notificationService.info(nls.localize('wordWrap.notInDiffEditor', "Cannot toggle word wrap in a diff editor."));
A
Alex Dima 已提交
146 147 148
			return;
		}

149
		const textResourceConfigurationService = accessor.get(ITextResourceConfigurationService);
150
		const codeEditorService = accessor.get(ICodeEditorService);
151 152
		const model = editor.getModel();

153 154 155 156
		if (!canToggleWordWrap(model.uri)) {
			return;
		}

157
		// Read the current state
158
		const currentState = readWordWrapState(model, textResourceConfigurationService, codeEditorService);
159 160 161
		// Compute the new state
		const newState = toggleWordWrap(editor, currentState);
		// Write the new state
A
Alex Dima 已提交
162
		// (this will cause an event and the controller will apply the state)
163
		writeTransientState(model, newState.transientState, codeEditorService);
I
ivanixgames 已提交
164 165
	}
}
166 167 168

class ToggleWordWrapController extends Disposable implements IEditorContribution {

169
	private static readonly _ID = 'editor.contrib.toggleWordWrapController';
170 171

	constructor(
172
		private readonly editor: ICodeEditor,
173
		@IContextKeyService readonly contextKeyService: IContextKeyService,
174
		@ITextResourceConfigurationService readonly configurationService: ITextResourceConfigurationService,
175
		@ICodeEditorService readonly codeEditorService: ICodeEditorService
176 177 178
	) {
		super();

A
Alex Dima 已提交
179
		const options = this.editor.getOptions();
A
renames  
Alex Dima 已提交
180
		const wrappingInfo = options.get(EditorOption.wrappingInfo);
A
Alex Dima 已提交
181 182
		const isWordWrapMinified = this.contextKeyService.createKey(isWordWrapMinifiedKey, wrappingInfo.isWordWrapMinified);
		const isDominatedByLongLines = this.contextKeyService.createKey(isDominatedByLongLinesKey, wrappingInfo.isDominatedByLongLines);
A
renames  
Alex Dima 已提交
183
		const inDiffEditor = this.contextKeyService.createKey(inDiffEditorKey, options.get(EditorOption.inDiffEditor));
A
Alex Dima 已提交
184
		let currentlyApplyingEditorConfig = false;
185 186

		this._register(editor.onDidChangeConfiguration((e) => {
187
			if (!e.hasChanged(EditorOption.wrappingInfo) && !e.hasChanged(EditorOption.inDiffEditor)) {
188 189
				return;
			}
A
Alex Dima 已提交
190
			const options = this.editor.getOptions();
A
renames  
Alex Dima 已提交
191
			const wrappingInfo = options.get(EditorOption.wrappingInfo);
A
Alex Dima 已提交
192 193
			isWordWrapMinified.set(wrappingInfo.isWordWrapMinified);
			isDominatedByLongLines.set(wrappingInfo.isDominatedByLongLines);
A
renames  
Alex Dima 已提交
194
			inDiffEditor.set(options.get(EditorOption.inDiffEditor));
A
Alex Dima 已提交
195 196 197 198
			if (!currentlyApplyingEditorConfig) {
				// I am not the cause of the word wrap getting changed
				ensureWordWrapSettings();
			}
199
		}));
200 201

		this._register(editor.onDidChangeModel((e) => {
A
Alex Dima 已提交
202 203 204
			ensureWordWrapSettings();
		}));

A
Alex Dima 已提交
205 206 207 208
		this._register(codeEditorService.onDidChangeTransientModelProperty(() => {
			ensureWordWrapSettings();
		}));

A
Alex Dima 已提交
209
		const ensureWordWrapSettings = () => {
210 211 212 213
			if (this.editor.getContribution(DefaultSettingsEditorContribution.ID)) {
				// in the settings editor...
				return;
			}
214 215 216 217 218 219
			// Ensure correct word wrap settings
			const newModel = this.editor.getModel();
			if (!newModel) {
				return;
			}

A
renames  
Alex Dima 已提交
220
			if (this.editor.getOption(EditorOption.inDiffEditor)) {
221 222 223
				return;
			}

224 225 226 227
			if (!canToggleWordWrap(newModel.uri)) {
				return;
			}

228 229 230 231
			// Read current configured values and toggle state
			const desiredState = readWordWrapState(newModel, this.configurationService, this.codeEditorService);

			// Apply the state
A
Alex Dima 已提交
232 233
			try {
				currentlyApplyingEditorConfig = true;
A
Alex Dima 已提交
234
				this._applyWordWrapState(desiredState);
A
Alex Dima 已提交
235 236 237 238
			} finally {
				currentlyApplyingEditorConfig = false;
			}
		};
239 240
	}

A
Alex Dima 已提交
241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257
	private _applyWordWrapState(state: IWordWrapState): void {
		if (state.transientState) {
			// toggle is on
			this.editor.updateOptions({
				wordWrap: state.transientState.forceWordWrap,
				wordWrapMinified: state.transientState.forceWordWrapMinified
			});
			return;
		}

		// toggle is off
		this.editor.updateOptions({
			wordWrap: state.configuredWordWrap,
			wordWrapMinified: state.configuredWordWrapMinified
		});
	}

258 259 260 261 262
	public getId(): string {
		return ToggleWordWrapController._ID;
	}
}

263 264 265 266
function canToggleWordWrap(uri: URI): boolean {
	if (!uri) {
		return false;
	}
267
	return (uri.scheme !== 'output');
268 269
}

270

271
registerEditorContribution(ToggleWordWrapController);
272

273
registerEditorAction(ToggleWordWrapAction);
274

275 276 277
const WORD_WRAP_DARK_ICON = URI.parse(registerAndGetAmdImageURL('vs/workbench/contrib/codeEditor/browser/word-wrap-dark.svg'));
const WORD_WRAP_LIGHT_ICON = URI.parse(registerAndGetAmdImageURL('vs/workbench/contrib/codeEditor/browser/word-wrap-light.svg'));

278 279
MenuRegistry.appendMenuItem(MenuId.EditorTitle, {
	command: {
280
		id: TOGGLE_WORD_WRAP_ID,
A
Alex Dima 已提交
281
		title: nls.localize('unwrapMinified', "Disable wrapping for this file"),
282
		iconLocation: {
283 284
			dark: WORD_WRAP_DARK_ICON,
			light: WORD_WRAP_LIGHT_ICON
285
		}
A
Alex Dima 已提交
286 287 288 289
	},
	group: 'navigation',
	order: 1,
	when: ContextKeyExpr.and(
290 291 292
		ContextKeyExpr.not(inDiffEditorKey),
		ContextKeyExpr.has(isDominatedByLongLinesKey),
		ContextKeyExpr.has(isWordWrapMinifiedKey)
A
Alex Dima 已提交
293 294 295 296
	)
});
MenuRegistry.appendMenuItem(MenuId.EditorTitle, {
	command: {
297
		id: TOGGLE_WORD_WRAP_ID,
A
Alex Dima 已提交
298
		title: nls.localize('wrapMinified', "Enable wrapping for this file"),
299
		iconLocation: {
300 301
			dark: WORD_WRAP_DARK_ICON,
			light: WORD_WRAP_LIGHT_ICON
302
		}
303 304 305
	},
	group: 'navigation',
	order: 1,
A
Alex Dima 已提交
306
	when: ContextKeyExpr.and(
307 308 309
		ContextKeyExpr.not(inDiffEditorKey),
		ContextKeyExpr.has(isDominatedByLongLinesKey),
		ContextKeyExpr.not(isWordWrapMinifiedKey)
A
Alex Dima 已提交
310
	)
311
});
312 313 314 315 316 317 318 319 320 321 322


// View menu
MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, {
	group: '5_editor',
	command: {
		id: TOGGLE_WORD_WRAP_ID,
		title: nls.localize({ key: 'miToggleWordWrap', comment: ['&& denotes a mnemonic'] }, "Toggle &&Word Wrap")
	},
	order: 1
});