deploy: current vibn theia state
Some checks failed
Playwright Tests / Playwright Tests (ubuntu-22.04, Node.js 22.x) (push) Has been cancelled
3PP License Check / 3PP License Check (11, 22.x, ubuntu-22.04) (push) Has been cancelled
Publish packages to NPM / Perform Publishing (push) Has been cancelled

Made-with: Cursor
This commit is contained in:
2026-02-27 12:01:08 -08:00
commit 8bb5110148
3782 changed files with 640947 additions and 0 deletions

View File

@@ -0,0 +1,10 @@
/** @type {import('eslint').Linter.Config} */
module.exports = {
extends: [
'../../configs/build.eslintrc.json'
],
parserOptions: {
tsconfigRootDir: __dirname,
project: 'tsconfig.json'
}
};

31
packages/editor/README.md Normal file
View File

@@ -0,0 +1,31 @@
<div align='center'>
<br />
<img src='https://raw.githubusercontent.com/eclipse-theia/theia/master/logo/theia.svg?sanitize=true' alt='theia-ext-logo' width='100px' />
<h2>ECLIPSE THEIA - EDITOR EXTENSION</h2>
<hr />
</div>
## Description
The `@theia/editor` extension contributed functionality such as the `editor` widget, menu, keybindings, and navigation.
## Additional Information
- [API documentation for `@theia/editor`](https://eclipse-theia.github.io/theia/docs/next/modules/_theia_editor.html)
- [Theia - GitHub](https://github.com/eclipse-theia/theia)
- [Theia - Website](https://theia-ide.org/)
## License
- [Eclipse Public License 2.0](http://www.eclipse.org/legal/epl-2.0/)
- [一 (Secondary) GNU General Public License, version 2 with the GNU Classpath Exception](https://projects.eclipse.org/license/secondary-gpl-2.0-cp)
## Trademark
"Theia" is a trademark of the Eclipse Foundation
<https://www.eclipse.org/theia>

View File

@@ -0,0 +1,51 @@
{
"name": "@theia/editor",
"version": "1.68.0",
"description": "Theia - Editor Extension",
"dependencies": {
"@theia/core": "1.68.0",
"@theia/variable-resolver": "1.68.0",
"tslib": "^2.6.2"
},
"publishConfig": {
"access": "public"
},
"theiaExtensions": [
{
"frontend": "lib/browser/editor-frontend-module",
"backend": "lib/node/editor-backend-module",
"secondaryWindow": "lib/browser/editor-frontend-module"
}
],
"keywords": [
"theia-extension"
],
"license": "EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0",
"repository": {
"type": "git",
"url": "https://github.com/eclipse-theia/theia.git"
},
"bugs": {
"url": "https://github.com/eclipse-theia/theia/issues"
},
"homepage": "https://github.com/eclipse-theia/theia",
"files": [
"lib",
"src"
],
"scripts": {
"build": "theiaext build",
"clean": "theiaext clean",
"compile": "theiaext compile",
"lint": "theiaext lint",
"test": "theiaext test",
"watch": "theiaext watch"
},
"devDependencies": {
"@theia/ext-scripts": "1.68.0"
},
"nyc": {
"extends": "../../configs/nyc.json"
},
"gitHead": "21358137e41342742707f660b8e222f940a27652"
}

View File

@@ -0,0 +1,41 @@
// *****************************************************************************
// Copyright (C) 2018 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { Disposable } from '@theia/core';
import { DecorationStyle } from '@theia/core/lib/browser';
export class EditorDecorationStyle implements Disposable {
constructor(
readonly selector: string,
styleProvider: (style: CSSStyleDeclaration) => void,
protected decorationsStyleSheet: CSSStyleSheet
) {
const styleRule = DecorationStyle.getOrCreateStyleRule(selector, decorationsStyleSheet);
if (styleRule) {
styleProvider(styleRule.style);
}
}
get className(): string {
return this.selector.split('::')[0];
}
dispose(): void {
DecorationStyle.deleteStyleRule(this.selector, this.decorationsStyleSheet);
}
}

View File

@@ -0,0 +1,140 @@
// *****************************************************************************
// Copyright (C) 2018 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { Range } from '@theia/core/shared/vscode-languageserver-protocol';
export interface EditorDecoration {
/**
* range to which this decoration instance is applied.
*/
range: Range;
/**
* options to be applied with this decoration.
*/
options: EditorDecorationOptions
}
export interface EditorDecorationOptions {
/**
* behavior of decorations when typing/editing near their edges.
*/
stickiness?: TrackedRangeStickiness;
/**
* CSS class name of this decoration.
*/
className?: string;
/**
* hover message for this decoration.
*/
hoverMessage?: string;
/**
* the decoration will be rendered in the glyph margin with this class name.
*/
glyphMarginClassName?: string;
/**
* hover message for the glyph margin of this decoration.
*/
glyphMarginHoverMessage?: string;
/**
* should the decoration be rendered for the whole line.
*/
isWholeLine?: boolean;
/**
* the decoration will be rendered in the lines decorations with this class name.
*/
linesDecorationsClassName?: string;
/**
* the decoration will be rendered in the margin in full width with this class name.
*/
marginClassName?: string;
/**
* the decoration will be rendered inline with this class name.
* to be used only to change text, otherwise use `className`.
*/
inlineClassName?: string;
/**
* the decoration will be rendered before the text with this class name.
*/
beforeContentClassName?: string;
/**
* the decoration will be rendered after the text with this class name.
*/
afterContentClassName?: string;
/**
* render this decoration in the overview ruler.
*/
overviewRuler?: DecorationOverviewRulerOptions;
/**
* If set, render this decoration in the minimap.
*/
minimap?: DecorationMinimapOptions;
blockClassName?: string;
blockPadding?: [top: number, right: number, bottom: number, left: number];
blockDoesNotCollapse?: boolean
/**
* Indicates if this block should be rendered after the last line.
* In this case, the range must be empty and set to the last line.
*/
blockIsAfterEnd?: boolean;
/**
* Always render the decoration (even when the range it encompasses is collapsed).
*/
showIfCollapsed?: boolean;
}
export interface DecorationOptions {
/**
* color of the decoration in the overview ruler.
* use `rgba` values to play well with other decorations.
*/
color: string | { id: string } | undefined;
/**
* The color to use in dark themes. Will be favored over `color` except in light themes.
*/
darkColor?: string | { id: string };
}
export enum MinimapPosition {
Inline = 1,
Gutter = 2
}
export interface DecorationMinimapOptions extends DecorationOptions {
position: MinimapPosition;
}
export interface DecorationOverviewRulerOptions extends DecorationOptions {
/**
* position in the overview ruler.
*/
position: OverviewRulerLane;
}
export enum OverviewRulerLane {
Left = 1,
Center = 2,
Right = 4,
Full = 7
}
export enum TrackedRangeStickiness {
AlwaysGrowsWhenTypingAtEdges = 0,
NeverGrowsWhenTypingAtEdges = 1,
GrowsOnlyWhenTypingBefore = 2,
GrowsOnlyWhenTypingAfter = 3,
}

View File

@@ -0,0 +1,36 @@
// *****************************************************************************
// Copyright (C) 2018 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { injectable } from '@theia/core/shared/inversify';
import { TextEditor } from '../editor';
import { EditorDecoration } from './editor-decoration';
@injectable()
export abstract class EditorDecorator {
protected readonly appliedDecorations = new Map<string, string[]>();
protected setDecorations(editor: TextEditor, newDecorations: EditorDecoration[]): void {
const uri = editor.uri.toString();
const oldDecorations = this.appliedDecorations.get(uri) || [];
if (oldDecorations.length === 0 && newDecorations.length === 0) {
return;
}
const decorationIds = editor.deltaDecorations({ oldDecorations, newDecorations });
this.appliedDecorations.set(uri, decorationIds);
}
}

View File

@@ -0,0 +1,19 @@
// *****************************************************************************
// Copyright (C) 2018 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
export * from './editor-decoration';
export * from './editor-decoration-style';
export * from './editor-decorator';

View File

@@ -0,0 +1,27 @@
// *****************************************************************************
// Copyright (C) 2018 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { TextEditor } from './editor';
export interface DiffNavigator {
hasNext(): boolean;
hasPrevious(): boolean;
next(): void;
previous(): void;
}
export const DiffNavigatorProvider = Symbol('DiffNavigatorProvider');
export type DiffNavigatorProvider = (editor: TextEditor) => DiffNavigator;

View File

@@ -0,0 +1,394 @@
// *****************************************************************************
// Copyright (C) 2017 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { inject, injectable, optional, postConstruct } from '@theia/core/shared/inversify';
import { CommonCommands, LabelProvider, ApplicationShell, QuickInputService, QuickPickValue, SaveableService } from '@theia/core/lib/browser';
import { EditorManager } from './editor-manager';
import { CommandContribution, CommandRegistry, Command, ResourceProvider, MessageService, nls } from '@theia/core';
import { LanguageService } from '@theia/core/lib/browser/language-service';
import { SUPPORTED_ENCODINGS } from '@theia/core/lib/common/supported-encodings';
import { EncodingMode } from './editor';
import { EditorLanguageQuickPickService } from './editor-language-quick-pick-service';
import { PreferenceService } from '@theia/core/lib/common/preferences';
export namespace EditorCommands {
const EDITOR_CATEGORY = 'Editor';
const EDITOR_CATEGORY_KEY = nls.getDefaultKey(EDITOR_CATEGORY);
export const GOTO_LINE_COLUMN = Command.toDefaultLocalizedCommand({
id: 'editor.action.gotoLine',
label: 'Go to Line/Column'
});
/**
* Show editor references
*/
export const SHOW_REFERENCES: Command = {
id: 'textEditor.commands.showReferences'
};
/**
* Change indentation configuration (i.e., indent using tabs / spaces, and how many spaces per tab)
*/
export const CONFIG_INDENTATION: Command = {
id: 'textEditor.commands.configIndentation'
};
export const CONFIG_EOL = Command.toDefaultLocalizedCommand({
id: 'textEditor.commands.configEol',
category: EDITOR_CATEGORY,
label: 'Change End of Line Sequence'
});
export const INDENT_USING_SPACES = Command.toDefaultLocalizedCommand({
id: 'textEditor.commands.indentUsingSpaces',
category: EDITOR_CATEGORY,
label: 'Indent Using Spaces'
});
export const INDENT_USING_TABS = Command.toDefaultLocalizedCommand({
id: 'textEditor.commands.indentUsingTabs',
category: EDITOR_CATEGORY,
label: 'Indent Using Tabs'
});
export const CHANGE_LANGUAGE = Command.toDefaultLocalizedCommand({
id: 'textEditor.change.language',
category: EDITOR_CATEGORY,
label: 'Change Language Mode'
});
export const CHANGE_ENCODING = Command.toDefaultLocalizedCommand({
id: 'textEditor.change.encoding',
category: EDITOR_CATEGORY,
label: 'Change File Encoding'
});
export const REVERT_EDITOR = Command.toDefaultLocalizedCommand({
id: 'workbench.action.files.revert',
category: CommonCommands.FILE_CATEGORY,
label: 'Revert File',
});
export const REVERT_AND_CLOSE = Command.toDefaultLocalizedCommand({
id: 'workbench.action.revertAndCloseActiveEditor',
category: CommonCommands.VIEW_CATEGORY,
label: 'Revert and Close Editor'
});
/**
* Command for going back to the last editor navigation location.
*/
export const GO_BACK = Command.toDefaultLocalizedCommand({
id: 'textEditor.commands.go.back',
category: EDITOR_CATEGORY,
label: 'Go Back'
});
/**
* Command for going to the forthcoming editor navigation location.
*/
export const GO_FORWARD = Command.toDefaultLocalizedCommand({
id: 'textEditor.commands.go.forward',
category: EDITOR_CATEGORY,
label: 'Go Forward'
});
/**
* Command that reveals the last text edit location, if any.
*/
export const GO_LAST_EDIT = Command.toDefaultLocalizedCommand({
id: 'textEditor.commands.go.lastEdit',
category: EDITOR_CATEGORY,
label: 'Go to Last Edit Location'
});
/**
* Command that clears the editor navigation history.
*/
export const CLEAR_EDITOR_HISTORY = Command.toDefaultLocalizedCommand({
id: 'textEditor.commands.clear.history',
category: EDITOR_CATEGORY,
label: 'Clear Editor History'
});
/**
* Command that displays all editors that are currently opened.
*/
export const SHOW_ALL_OPENED_EDITORS = Command.toLocalizedCommand({
id: 'workbench.action.showAllEditors',
category: CommonCommands.VIEW_CATEGORY,
label: 'Show All Opened Editors'
}, 'theia/editor/showAllEditors', EDITOR_CATEGORY_KEY);
/**
* Command that toggles the minimap.
*/
export const TOGGLE_MINIMAP = Command.toDefaultLocalizedCommand({
id: 'editor.action.toggleMinimap',
category: CommonCommands.VIEW_CATEGORY,
label: 'Toggle Minimap'
});
/**
* Command that toggles the rendering of whitespace characters in the editor.
*/
export const TOGGLE_RENDER_WHITESPACE = Command.toDefaultLocalizedCommand({
id: 'editor.action.toggleRenderWhitespace',
category: CommonCommands.VIEW_CATEGORY,
label: 'Toggle Render Whitespace'
});
/**
* Command that toggles the word wrap.
*/
export const TOGGLE_WORD_WRAP = Command.toDefaultLocalizedCommand({
id: 'editor.action.toggleWordWrap',
label: 'View: Toggle Word Wrap'
});
/**
* Command that toggles sticky scroll.
*/
export const TOGGLE_STICKY_SCROLL = Command.toLocalizedCommand({
id: 'editor.action.toggleStickyScroll',
category: CommonCommands.VIEW_CATEGORY,
label: 'Toggle Sticky Scroll',
}, 'theia/editor/toggleStickyScroll', EDITOR_CATEGORY_KEY);
/**
* Command that re-opens the last closed editor.
*/
export const REOPEN_CLOSED_EDITOR = Command.toDefaultLocalizedCommand({
id: 'workbench.action.reopenClosedEditor',
category: CommonCommands.VIEW_CATEGORY,
label: 'Reopen Closed Editor'
});
/**
* Opens a second instance of the current editor, splitting the view in the direction specified.
*/
export const SPLIT_EDITOR_RIGHT = Command.toDefaultLocalizedCommand({
id: 'workbench.action.splitEditorRight',
category: CommonCommands.VIEW_CATEGORY,
label: 'Split Editor Right'
});
export const SPLIT_EDITOR_DOWN = Command.toDefaultLocalizedCommand({
id: 'workbench.action.splitEditorDown',
category: CommonCommands.VIEW_CATEGORY,
label: 'Split Editor Down'
});
export const SPLIT_EDITOR_UP = Command.toDefaultLocalizedCommand({
id: 'workbench.action.splitEditorUp',
category: CommonCommands.VIEW_CATEGORY,
label: 'Split Editor Up'
});
export const SPLIT_EDITOR_LEFT = Command.toDefaultLocalizedCommand({
id: 'workbench.action.splitEditorLeft',
category: CommonCommands.VIEW_CATEGORY,
label: 'Split Editor Left'
});
/**
* Default horizontal split: right.
*/
export const SPLIT_EDITOR_HORIZONTAL = Command.toDefaultLocalizedCommand({
id: 'workbench.action.splitEditor',
category: CommonCommands.VIEW_CATEGORY,
label: 'Split Editor'
});
/**
* Default vertical split: down.
*/
export const SPLIT_EDITOR_VERTICAL = Command.toDefaultLocalizedCommand({
id: 'workbench.action.splitEditorOrthogonal',
category: CommonCommands.VIEW_CATEGORY,
label: 'Split Editor Orthogonal'
});
}
@injectable()
export class EditorCommandContribution implements CommandContribution {
static readonly AUTOSAVE_PREFERENCE: string = 'files.autoSave';
static readonly AUTOSAVE_DELAY_PREFERENCE: string = 'files.autoSaveDelay';
@inject(ApplicationShell)
protected readonly shell: ApplicationShell;
@inject(PreferenceService)
protected readonly preferencesService: PreferenceService;
@inject(SaveableService)
protected readonly saveResourceService: SaveableService;
@inject(QuickInputService) @optional()
protected readonly quickInputService: QuickInputService;
@inject(MessageService)
protected readonly messageService: MessageService;
@inject(LabelProvider)
protected readonly labelProvider: LabelProvider;
@inject(LanguageService)
protected readonly languages: LanguageService;
@inject(EditorManager)
protected readonly editorManager: EditorManager;
@inject(ResourceProvider)
protected readonly resourceProvider: ResourceProvider;
@inject(EditorLanguageQuickPickService)
protected readonly codeLanguageQuickPickService: EditorLanguageQuickPickService;
@postConstruct()
protected init(): void {
this.preferencesService.ready.then(() => {
this.saveResourceService.autoSave = this.preferencesService.get(EditorCommandContribution.AUTOSAVE_PREFERENCE) ?? 'off';
this.saveResourceService.autoSaveDelay = this.preferencesService.get(EditorCommandContribution.AUTOSAVE_DELAY_PREFERENCE) ?? 1000;
});
this.preferencesService.onPreferenceChanged(e => {
if (e.preferenceName === EditorCommandContribution.AUTOSAVE_PREFERENCE) {
this.saveResourceService.autoSave = this.preferencesService.get(EditorCommandContribution.AUTOSAVE_PREFERENCE) ?? 'off';
} else if (e.preferenceName === EditorCommandContribution.AUTOSAVE_DELAY_PREFERENCE) {
this.saveResourceService.autoSaveDelay = this.preferencesService.get(EditorCommandContribution.AUTOSAVE_DELAY_PREFERENCE) ?? 1000;
}
});
}
registerCommands(registry: CommandRegistry): void {
registry.registerCommand(EditorCommands.SHOW_REFERENCES);
registry.registerCommand(EditorCommands.CONFIG_INDENTATION);
registry.registerCommand(EditorCommands.CONFIG_EOL);
registry.registerCommand(EditorCommands.INDENT_USING_SPACES);
registry.registerCommand(EditorCommands.INDENT_USING_TABS);
registry.registerCommand(EditorCommands.REVERT_EDITOR);
registry.registerCommand(EditorCommands.REVERT_AND_CLOSE);
registry.registerCommand(EditorCommands.CHANGE_LANGUAGE, {
isEnabled: () => this.canConfigureLanguage(),
isVisible: () => this.canConfigureLanguage(),
execute: () => this.configureLanguage()
});
registry.registerCommand(EditorCommands.CHANGE_ENCODING, {
isEnabled: () => this.canConfigureEncoding(),
isVisible: () => this.canConfigureEncoding(),
execute: () => this.configureEncoding()
});
registry.registerCommand(EditorCommands.GO_BACK);
registry.registerCommand(EditorCommands.GO_FORWARD);
registry.registerCommand(EditorCommands.GO_LAST_EDIT);
registry.registerCommand(EditorCommands.CLEAR_EDITOR_HISTORY);
registry.registerCommand(EditorCommands.TOGGLE_MINIMAP);
registry.registerCommand(EditorCommands.TOGGLE_RENDER_WHITESPACE);
registry.registerCommand(EditorCommands.TOGGLE_WORD_WRAP);
registry.registerCommand(EditorCommands.TOGGLE_STICKY_SCROLL);
registry.registerCommand(EditorCommands.REOPEN_CLOSED_EDITOR);
registry.registerCommand(CommonCommands.AUTO_SAVE, {
isToggled: () => this.isAutoSaveOn(),
execute: () => this.toggleAutoSave()
});
}
protected canConfigureLanguage(): boolean {
const widget = this.editorManager.currentEditor;
const editor = widget && widget.editor;
return !!editor && !!this.languages.languages;
}
protected async configureLanguage(): Promise<void> {
const widget = this.editorManager.currentEditor;
const editor = widget && widget.editor;
if (!editor || !this.languages.languages) {
return;
}
const current = editor.document.languageId;
const selectedMode = await this.codeLanguageQuickPickService.pickEditorLanguage(current);
if (selectedMode && ('value' in selectedMode)) {
if (selectedMode.value === 'autoDetect') {
editor.detectLanguage();
} else if (selectedMode.value) {
editor.setLanguage(selectedMode.value.id);
}
}
}
protected canConfigureEncoding(): boolean {
const widget = this.editorManager.currentEditor;
const editor = widget && widget.editor;
return !!editor;
}
protected async configureEncoding(): Promise<void> {
const widget = this.editorManager.currentEditor;
const editor = widget && widget.editor;
if (!editor) {
return;
}
const reopenWithEncodingPick = { label: nls.localizeByDefault('Reopen with Encoding'), value: 'reopen' };
const saveWithEncodingPick = { label: nls.localizeByDefault('Save with Encoding'), value: 'save' };
const actionItems: QuickPickValue<string>[] = [
reopenWithEncodingPick,
saveWithEncodingPick
];
const selectedEncoding = await this.quickInputService?.showQuickPick(actionItems, { placeholder: nls.localizeByDefault('Select Action') });
if (!selectedEncoding) {
return;
}
const isReopenWithEncoding = (selectedEncoding.value === reopenWithEncodingPick.value);
const configuredEncoding = this.preferencesService.get<string>('files.encoding', 'utf8', editor.uri.toString());
const resource = await this.resourceProvider(editor.uri);
const guessedEncoding = resource.guessEncoding ? await resource.guessEncoding() : undefined;
resource.dispose();
const encodingItems: QuickPickValue<{ id: string, description: string }>[] = Object.keys(SUPPORTED_ENCODINGS)
.sort((k1, k2) => {
if (k1 === configuredEncoding) {
return -1;
} else if (k2 === configuredEncoding) {
return 1;
}
return SUPPORTED_ENCODINGS[k1].order - SUPPORTED_ENCODINGS[k2].order;
})
.filter(k => {
if (k === guessedEncoding && guessedEncoding !== configuredEncoding) {
return false; // do not show encoding if it is the guessed encoding that does not match the configured
}
return !isReopenWithEncoding || !SUPPORTED_ENCODINGS[k].encodeOnly; // hide those that can only be used for encoding if we are about to decode
})
.map(key => ({ label: SUPPORTED_ENCODINGS[key].labelLong, value: { id: key, description: key } }));
// Insert guessed encoding
if (guessedEncoding && configuredEncoding !== guessedEncoding && SUPPORTED_ENCODINGS[guessedEncoding]) {
encodingItems.unshift({
label: `${nls.localizeByDefault('Guessed from content')}: ${SUPPORTED_ENCODINGS[guessedEncoding].labelLong}`,
value: { id: guessedEncoding, description: guessedEncoding }
});
}
const selectedFileEncoding = await this.quickInputService?.showQuickPick<QuickPickValue<{ id: string, description: string }>>(encodingItems, {
placeholder: isReopenWithEncoding ?
nls.localizeByDefault('Select File Encoding to Reopen File') :
nls.localizeByDefault('Select File Encoding to Save with')
});
if (!selectedFileEncoding) {
return;
}
if (editor.document.dirty && isReopenWithEncoding) {
this.messageService.info(nls.localize('theia/editor/dirtyEncoding', 'The file is dirty. Please save it first before reopening it with another encoding.'));
return;
} else if (selectedFileEncoding.value) {
editor.setEncoding(selectedFileEncoding.value.id, isReopenWithEncoding ? EncodingMode.Decode : EncodingMode.Encode);
}
}
protected isAutoSaveOn(): boolean {
const autoSave = this.preferencesService.get(EditorCommandContribution.AUTOSAVE_PREFERENCE);
return autoSave !== 'off';
}
protected async toggleAutoSave(): Promise<void> {
this.preferencesService.updateValue(EditorCommandContribution.AUTOSAVE_PREFERENCE, this.isAutoSaveOn() ? 'off' : 'afterDelay');
}
}

View File

@@ -0,0 +1,223 @@
// *****************************************************************************
// Copyright (C) 2017 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { EditorManager } from './editor-manager';
import { TextEditor } from './editor';
import { injectable, inject, optional, named } from '@theia/core/shared/inversify';
import { StatusBarAlignment, StatusBar } from '@theia/core/lib/browser/status-bar/status-bar';
import {
FrontendApplicationContribution, DiffUris, DockLayout,
QuickInputService, KeybindingRegistry, KeybindingContribution, SHELL_TABBAR_CONTEXT_SPLIT, ApplicationShell,
WidgetStatusBarContribution,
Widget,
OpenWithService
} from '@theia/core/lib/browser';
import { ContextKeyService } from '@theia/core/lib/browser/context-key-service';
import { CommandHandler, DisposableCollection, MenuContribution, MenuModelRegistry, ContributionProvider, Prioritizeable } from '@theia/core';
import { EditorCommands } from './editor-command';
import { CommandRegistry, CommandContribution } from '@theia/core/lib/common';
import { SUPPORTED_ENCODINGS } from '@theia/core/lib/common/supported-encodings';
import { nls } from '@theia/core/lib/common/nls';
import { CurrentWidgetCommandAdapter } from '@theia/core/lib/browser/shell/current-widget-command-adapter';
import { EditorWidget } from './editor-widget';
import { EditorLanguageStatusService } from './language-status/editor-language-status-service';
import { QuickEditorService } from './quick-editor-service';
import { SplitEditorContribution } from './split-editor-contribution';
@injectable()
export class EditorContribution implements FrontendApplicationContribution,
CommandContribution, KeybindingContribution, MenuContribution, WidgetStatusBarContribution<EditorWidget> {
@inject(EditorManager) protected readonly editorManager: EditorManager;
@inject(OpenWithService) protected readonly openWithService: OpenWithService;
@inject(EditorLanguageStatusService) protected readonly languageStatusService: EditorLanguageStatusService;
@inject(ApplicationShell) protected readonly shell: ApplicationShell;
@inject(ContextKeyService)
protected readonly contextKeyService: ContextKeyService;
@inject(QuickInputService) @optional()
protected readonly quickInputService: QuickInputService;
@inject(ContributionProvider) @named(SplitEditorContribution)
protected readonly splitEditorContributions: ContributionProvider<SplitEditorContribution>;
onStart(): void {
this.initEditorContextKeys();
this.openWithService.registerHandler({
id: 'default',
label: this.editorManager.label,
providerName: nls.localizeByDefault('Built-in'),
canHandle: () => 100,
// Higher priority than any other handler
// so that the text editor always appears first in the quick pick
getOrder: () => 10000,
open: uri => this.editorManager.open(uri)
});
}
protected initEditorContextKeys(): void {
const editorIsOpen = this.contextKeyService.createKey<boolean>('editorIsOpen', false);
const textCompareEditorVisible = this.contextKeyService.createKey<boolean>('textCompareEditorVisible', false);
const updateContextKeys = () => {
const widgets = this.editorManager.all;
editorIsOpen.set(!!widgets.length);
textCompareEditorVisible.set(widgets.some(widget => DiffUris.isDiffUri(widget.editor.uri)));
};
updateContextKeys();
for (const widget of this.editorManager.all) {
widget.disposed.connect(updateContextKeys);
}
this.editorManager.onCreated(widget => {
updateContextKeys();
widget.disposed.connect(updateContextKeys);
});
}
protected readonly toDisposeOnCurrentEditorChanged = new DisposableCollection();
canHandle(widget: Widget): widget is EditorWidget {
return widget instanceof EditorWidget;
}
activate(statusBar: StatusBar, widget: EditorWidget): void {
this.toDisposeOnCurrentEditorChanged.dispose();
const editor = widget.editor;
this.updateLanguageStatus(statusBar, editor);
this.updateEncodingStatus(statusBar, editor);
this.setCursorPositionStatus(statusBar, editor);
this.toDisposeOnCurrentEditorChanged.pushAll([
editor.onLanguageChanged(() => this.updateLanguageStatus(statusBar, editor)),
editor.onEncodingChanged(() => this.updateEncodingStatus(statusBar, editor)),
editor.onCursorPositionChanged(() => this.setCursorPositionStatus(statusBar, editor))
]);
}
deactivate(statusBar: StatusBar): void {
this.toDisposeOnCurrentEditorChanged.dispose();
this.updateLanguageStatus(statusBar, undefined);
this.updateEncodingStatus(statusBar, undefined);
this.setCursorPositionStatus(statusBar, undefined);
}
protected updateLanguageStatus(statusBar: StatusBar, editor: TextEditor | undefined): void {
this.languageStatusService.updateLanguageStatus(editor);
}
protected updateEncodingStatus(statusBar: StatusBar, editor: TextEditor | undefined): void {
if (!editor) {
statusBar.removeElement('editor-status-encoding');
return;
}
statusBar.setElement('editor-status-encoding', {
text: SUPPORTED_ENCODINGS[editor.getEncoding()].labelShort,
alignment: StatusBarAlignment.RIGHT,
priority: 10,
command: EditorCommands.CHANGE_ENCODING.id,
tooltip: nls.localizeByDefault('Select Encoding')
});
}
protected setCursorPositionStatus(statusBar: StatusBar, editor: TextEditor | undefined): void {
if (!editor) {
statusBar.removeElement('editor-status-cursor-position');
return;
}
const { cursor } = editor;
statusBar.setElement('editor-status-cursor-position', {
text: nls.localizeByDefault('Ln {0}, Col {1}', cursor.line + 1, editor.getVisibleColumn(cursor)),
alignment: StatusBarAlignment.RIGHT,
priority: 100,
tooltip: EditorCommands.GOTO_LINE_COLUMN.label,
command: EditorCommands.GOTO_LINE_COLUMN.id
});
}
registerCommands(commands: CommandRegistry): void {
commands.registerCommand(EditorCommands.SHOW_ALL_OPENED_EDITORS, {
execute: () => this.quickInputService?.open(QuickEditorService.PREFIX)
});
const splitHandlerFactory = (splitMode: DockLayout.InsertMode): CommandHandler => new CurrentWidgetCommandAdapter(this.shell, {
isEnabled: title => {
if (!title?.owner) {
return false;
}
return this.findSplitContribution(title.owner) !== undefined;
},
execute: async title => {
if (!title?.owner) {
return;
}
const contribution = this.findSplitContribution(title.owner);
if (contribution) {
await contribution.split(title.owner, splitMode);
}
}
});
commands.registerCommand(EditorCommands.SPLIT_EDITOR_HORIZONTAL, splitHandlerFactory('split-right'));
commands.registerCommand(EditorCommands.SPLIT_EDITOR_VERTICAL, splitHandlerFactory('split-bottom'));
commands.registerCommand(EditorCommands.SPLIT_EDITOR_RIGHT, splitHandlerFactory('split-right'));
commands.registerCommand(EditorCommands.SPLIT_EDITOR_DOWN, splitHandlerFactory('split-bottom'));
commands.registerCommand(EditorCommands.SPLIT_EDITOR_UP, splitHandlerFactory('split-top'));
commands.registerCommand(EditorCommands.SPLIT_EDITOR_LEFT, splitHandlerFactory('split-left'));
}
protected findSplitContribution(widget: Widget): SplitEditorContribution | undefined {
const prioritized = Prioritizeable.prioritizeAllSync(
this.splitEditorContributions.getContributions(),
contribution => contribution.canHandle(widget)
);
return prioritized.length > 0 ? prioritized[0].value : undefined;
}
registerKeybindings(keybindings: KeybindingRegistry): void {
keybindings.registerKeybinding({
command: EditorCommands.SHOW_ALL_OPENED_EDITORS.id,
keybinding: 'ctrlcmd+k ctrlcmd+p'
});
keybindings.registerKeybinding({
command: EditorCommands.SPLIT_EDITOR_HORIZONTAL.id,
keybinding: 'ctrlcmd+\\',
});
keybindings.registerKeybinding({
command: EditorCommands.SPLIT_EDITOR_VERTICAL.id,
keybinding: 'ctrlcmd+k ctrlcmd+\\',
});
}
registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(SHELL_TABBAR_CONTEXT_SPLIT, {
commandId: EditorCommands.SPLIT_EDITOR_UP.id,
label: nls.localizeByDefault('Split Up'),
order: '1',
});
registry.registerMenuAction(SHELL_TABBAR_CONTEXT_SPLIT, {
commandId: EditorCommands.SPLIT_EDITOR_DOWN.id,
label: nls.localizeByDefault('Split Down'),
order: '2',
});
registry.registerMenuAction(SHELL_TABBAR_CONTEXT_SPLIT, {
commandId: EditorCommands.SPLIT_EDITOR_LEFT.id,
label: nls.localizeByDefault('Split Left'),
order: '3',
});
registry.registerMenuAction(SHELL_TABBAR_CONTEXT_SPLIT, {
commandId: EditorCommands.SPLIT_EDITOR_RIGHT.id,
label: nls.localizeByDefault('Split Right'),
order: '4',
});
}
}

View File

@@ -0,0 +1,103 @@
// *****************************************************************************
// Copyright (C) 2018 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import '../../src/browser/style/index.css';
import '../../src/browser/language-status/editor-language-status.css';
import { ContainerModule } from '@theia/core/shared/inversify';
import { bindContributionProvider, CommandContribution, MenuContribution } from '@theia/core/lib/common';
import { OpenHandler, WidgetFactory, FrontendApplicationContribution, KeybindingContribution, WidgetStatusBarContribution } from '@theia/core/lib/browser';
import { VariableContribution } from '@theia/variable-resolver/lib/browser';
import { EditorManager, EditorAccess, ActiveEditorAccess, CurrentEditorAccess, EditorSelectionResolver } from './editor-manager';
import { EditorContribution } from './editor-contribution';
import { EditorMenuContribution } from './editor-menu';
import { EditorCommandContribution } from './editor-command';
import { EditorKeybindingContribution } from './editor-keybinding';
import { bindEditorPreferences } from '../common/editor-preferences';
import { EditorWidgetFactory } from './editor-widget-factory';
import { EditorNavigationContribution } from './editor-navigation-contribution';
import { NavigationLocationUpdater } from './navigation/navigation-location-updater';
import { NavigationLocationService } from './navigation/navigation-location-service';
import { NavigationLocationSimilarity } from './navigation/navigation-location-similarity';
import { EditorVariableContribution } from './editor-variable-contribution';
import { QuickAccessContribution } from '@theia/core/lib/browser/quick-input/quick-access';
import { QuickEditorService } from './quick-editor-service';
import { EditorLanguageStatusService } from './language-status/editor-language-status-service';
import { EditorLineNumberContribution } from './editor-linenumber-contribution';
import { UndoRedoService } from './undo-redo-service';
import { EditorLanguageQuickPickService } from './editor-language-quick-pick-service';
import { SplitEditorContribution } from './split-editor-contribution';
import { TextEditorSplitContribution } from './text-editor-split-contribution';
export default new ContainerModule(bind => {
bindEditorPreferences(bind);
bind(EditorWidgetFactory).toSelf().inSingletonScope();
bind(WidgetFactory).toService(EditorWidgetFactory);
bind(EditorManager).toSelf().inSingletonScope();
bind(OpenHandler).toService(EditorManager);
bindContributionProvider(bind, EditorSelectionResolver);
bindContributionProvider(bind, SplitEditorContribution);
bind(TextEditorSplitContribution).toSelf().inSingletonScope();
bind(SplitEditorContribution).toService(TextEditorSplitContribution);
bind(EditorCommandContribution).toSelf().inSingletonScope();
bind(CommandContribution).toService(EditorCommandContribution);
bind(EditorMenuContribution).toSelf().inSingletonScope();
bind(MenuContribution).toService(EditorMenuContribution);
bind(EditorKeybindingContribution).toSelf().inSingletonScope();
bind(KeybindingContribution).toService(EditorKeybindingContribution);
bind(EditorContribution).toSelf().inSingletonScope();
bind(EditorLanguageStatusService).toSelf().inSingletonScope();
bind(EditorLineNumberContribution).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toService(EditorLineNumberContribution);
bind(EditorNavigationContribution).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toService(EditorNavigationContribution);
bind(NavigationLocationService).toSelf().inSingletonScope();
bind(NavigationLocationUpdater).toSelf().inSingletonScope();
bind(NavigationLocationSimilarity).toSelf().inSingletonScope();
bind(VariableContribution).to(EditorVariableContribution).inSingletonScope();
[
FrontendApplicationContribution,
WidgetStatusBarContribution,
CommandContribution,
KeybindingContribution,
MenuContribution
].forEach(serviceIdentifier => {
bind(serviceIdentifier).toService(EditorContribution);
});
bind(QuickEditorService).toSelf().inSingletonScope();
bind(QuickAccessContribution).to(QuickEditorService);
bind(CurrentEditorAccess).toSelf().inSingletonScope();
bind(ActiveEditorAccess).toSelf().inSingletonScope();
bind(EditorAccess).to(CurrentEditorAccess).inSingletonScope().whenTargetNamed(EditorAccess.CURRENT);
bind(EditorAccess).to(ActiveEditorAccess).inSingletonScope().whenTargetNamed(EditorAccess.ACTIVE);
bind(UndoRedoService).toSelf().inSingletonScope();
bind(EditorLanguageQuickPickService).toSelf().inSingletonScope();
});

View File

@@ -0,0 +1,55 @@
// *****************************************************************************
// Copyright (C) 2018 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { injectable } from '@theia/core/shared/inversify';
import { environment } from '@theia/core/shared/@theia/application-package/lib/environment';
import { isOSX, isWindows } from '@theia/core/lib/common/os';
import { KeybindingContribution, KeybindingRegistry } from '@theia/core/lib/browser/keybinding';
import { EditorCommands } from './editor-command';
@injectable()
export class EditorKeybindingContribution implements KeybindingContribution {
registerKeybindings(registry: KeybindingRegistry): void {
registry.registerKeybindings(
{
command: EditorCommands.GO_BACK.id,
keybinding: isOSX ? 'ctrl+-' : isWindows ? 'alt+left' : /* isLinux */ 'ctrl+alt+-'
},
{
command: EditorCommands.GO_FORWARD.id,
keybinding: isOSX ? 'ctrl+shift+-' : isWindows ? 'alt+right' : /* isLinux */ 'ctrl+shift+-'
},
{
command: EditorCommands.GO_LAST_EDIT.id,
keybinding: 'ctrl+alt+q'
},
{
command: EditorCommands.TOGGLE_WORD_WRAP.id,
keybinding: 'alt+z'
},
{
command: EditorCommands.REOPEN_CLOSED_EDITOR.id,
keybinding: this.isElectron() ? 'ctrlcmd+shift+t' : 'alt+shift+t'
}
);
}
private isElectron(): boolean {
return environment.electron.is();
}
}

View File

@@ -0,0 +1,71 @@
// *****************************************************************************
// Copyright (C) 2024 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { inject, injectable } from '@theia/core/shared/inversify';
import { Language, LanguageService } from '@theia/core/lib/browser/language-service';
import { nls, QuickInputService, QuickPickItemOrSeparator, QuickPickValue, URI } from '@theia/core';
import { LabelProvider } from '@theia/core/lib/browser';
@injectable()
export class EditorLanguageQuickPickService {
@inject(LanguageService)
protected readonly languages: LanguageService;
@inject(QuickInputService)
protected readonly quickInputService: QuickInputService;
@inject(LabelProvider)
protected readonly labelProvider: LabelProvider;
async pickEditorLanguage(current: string): Promise<QuickPickValue<'autoDetect' | Language> | undefined> {
const items: Array<QuickPickValue<'autoDetect' | Language> | QuickPickItemOrSeparator> = [
{ label: nls.localizeByDefault('Auto Detect'), value: 'autoDetect' },
{ type: 'separator', label: nls.localizeByDefault('languages (identifier)') },
... (this.languages.languages.map(language => this.toQuickPickLanguage(language, current))).sort((e, e2) => e.label.localeCompare(e2.label))
];
const selectedMode = await this.quickInputService?.showQuickPick(items, { placeholder: nls.localizeByDefault('Select Language Mode') });
return (selectedMode && 'value' in selectedMode) ? selectedMode : undefined;
}
protected toQuickPickLanguage(value: Language, current: string): QuickPickValue<Language> {
const languageUri = this.toLanguageUri(value);
const iconClasses = this.labelProvider.getIcon(languageUri).split(' ').filter(v => v.length > 0);
if (iconClasses.length > 0) {
iconClasses.push('file-icon');
}
const configured = current === value.id;
return {
value,
label: value.name,
description: nls.localizeByDefault(`({0})${configured ? ' - Configured Language' : ''}`, value.id),
iconClasses
};
}
protected toLanguageUri(language: Language): URI {
const extension = language.extensions.values().next();
if (extension.value) {
return new URI('file:///' + extension.value);
}
const filename = language.filenames.values().next();
if (filename.value) {
return new URI('file:///' + filename.value);
}
return new URI('file:///.txt');
}
}

View File

@@ -0,0 +1,89 @@
// *****************************************************************************
// Copyright (C) 2023 STMicroelectronics and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { EditorManager } from './editor-manager';
import { EditorMouseEvent, MouseTargetType, Position, TextEditor } from './editor';
import { injectable, inject } from '@theia/core/shared/inversify';
import { FrontendApplicationContribution, ContextMenuRenderer } from '@theia/core/lib/browser';
import { ContextKeyService } from '@theia/core/lib/browser/context-key-service';
import { Disposable, DisposableCollection, MenuPath } from '@theia/core';
import { EditorWidget } from './editor-widget';
export const EDITOR_LINENUMBER_CONTEXT_MENU: MenuPath = ['editor_linenumber_context_menu'];
@injectable()
export class EditorLineNumberContribution implements FrontendApplicationContribution {
@inject(ContextKeyService)
protected readonly contextKeyService: ContextKeyService;
@inject(ContextMenuRenderer)
protected readonly contextMenuRenderer: ContextMenuRenderer;
@inject(EditorManager)
protected readonly editorManager: EditorManager;
onStart(): void {
this.editorManager.onCreated(editor => this.addLineNumberContextMenu(editor));
}
protected addLineNumberContextMenu(editorWidget: EditorWidget): void {
const editor = editorWidget.editor;
if (editor) {
const disposables = new DisposableCollection();
disposables.push(editor.onMouseDown(event => this.handleContextMenu(editor, event)));
const dispose = () => disposables.dispose();
editorWidget.disposed.connect(dispose);
disposables.push(Disposable.create(() => editorWidget.disposed.disconnect(dispose)));
}
}
protected handleContextMenu(editor: TextEditor, event: EditorMouseEvent): void {
if (event.target && (event.target.type === MouseTargetType.GUTTER_LINE_NUMBERS || event.target.type === MouseTargetType.GUTTER_GLYPH_MARGIN)) {
if (event.event.button === 2) {
editor.focus();
const lineNumber = lineNumberFromPosition(event.target.position);
const contextKeyService = this.contextKeyService.createOverlay([['editorLineNumber', lineNumber]]);
const uri = editor.getResourceUri()!;
const args = [{
lineNumber: lineNumber,
column: 1, // Compatible with Monaco editor IPosition API
uri: uri['codeUri'],
}];
setTimeout(() => {
this.contextMenuRenderer.render({
menuPath: EDITOR_LINENUMBER_CONTEXT_MENU,
anchor: event.event,
context: editor.node,
args,
contextKeyService
});
});
}
}
}
}
function lineNumberFromPosition(position: Position | undefined): number | undefined {
// position.line is 0-based line position, where the expected editor line number is 1-based.
if (position) {
return position.line + 1;
}
return undefined;
}

View File

@@ -0,0 +1,505 @@
// *****************************************************************************
// Copyright (C) 2017 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { injectable, postConstruct, inject, named } from '@theia/core/shared/inversify';
import URI from '@theia/core/lib/common/uri';
import { RecursivePartial, Emitter, Event, CommandService, nls, ContributionProvider, Prioritizeable, Disposable } from '@theia/core/lib/common';
import {
WidgetOpenerOptions, NavigatableWidgetOpenHandler, NavigatableWidgetOptions, CommonCommands, getDefaultHandler, defaultHandlerPriority, DiffUris
} from '@theia/core/lib/browser';
import { EditorWidget } from './editor-widget';
import { Range, Position, Location, TextEditor } from './editor';
import { EditorWidgetFactory } from './editor-widget-factory';
import { NavigationLocationService } from './navigation/navigation-location-service';
import { PreferenceService } from '@theia/core/lib/common/preferences';
export interface WidgetId {
id: number;
uri: string;
}
export interface EditorOpenerOptions extends WidgetOpenerOptions {
selection?: RecursivePartial<Range>;
revealOption?: 'auto' | 'center' | 'centerIfOutsideViewport'; // defaults to 'center'
preview?: boolean;
counter?: number;
}
export const EditorSelectionResolver = Symbol('EditorSelectionResolver');
export interface EditorSelectionResolver {
priority?: number;
resolveSelection(widget: EditorWidget, options: EditorOpenerOptions, uri?: URI): Promise<RecursivePartial<Range> | undefined>;
}
@injectable()
export class EditorManager extends NavigatableWidgetOpenHandler<EditorWidget> {
readonly id = EditorWidgetFactory.ID;
readonly label = nls.localizeByDefault('Text Editor');
protected readonly editorCounters = new Map<string, number>();
protected readonly onActiveEditorChangedEmitter = new Emitter<EditorWidget | undefined>();
/**
* Emit when the active editor is changed.
*/
readonly onActiveEditorChanged: Event<EditorWidget | undefined> = this.onActiveEditorChangedEmitter.event;
protected readonly onCurrentEditorChangedEmitter = new Emitter<EditorWidget | undefined>();
/**
* Emit when the current editor is changed.
*/
readonly onCurrentEditorChanged: Event<EditorWidget | undefined> = this.onCurrentEditorChangedEmitter.event;
@inject(CommandService) protected readonly commands: CommandService;
@inject(PreferenceService) protected readonly preferenceService: PreferenceService;
@inject(ContributionProvider) @named(EditorSelectionResolver)
protected readonly resolverContributions: ContributionProvider<EditorSelectionResolver>;
protected selectionResolvers: EditorSelectionResolver[] = [];
@inject(NavigationLocationService)
protected readonly navigationLocationService: NavigationLocationService;
@postConstruct()
protected override init(): void {
super.init();
this.selectionResolvers = Prioritizeable.prioritizeAllSync(
this.resolverContributions.getContributions(),
resolver => resolver.priority ?? 0
).map(p => p.value);
this.shell.onDidChangeActiveWidget(() => this.updateActiveEditor());
this.shell.onDidChangeCurrentWidget(() => this.updateCurrentEditor());
this.shell.onDidDoubleClickMainArea(() =>
this.commands.executeCommand(CommonCommands.NEW_UNTITLED_TEXT_FILE.id)
);
this.onCreated(widget => {
widget.onDidChangeVisibility(() => {
if (widget.isVisible) {
this.addRecentlyVisible(widget);
}
this.updateCurrentEditor();
});
this.checkCounterForWidget(widget);
widget.disposed.connect(() => {
this.removeFromCounter(widget);
this.removeRecentlyVisible(widget);
this.updateCurrentEditor();
});
});
for (const widget of this.all) {
if (widget.isVisible) {
this.addRecentlyVisible(widget);
}
}
this.updateCurrentEditor();
}
/**
* Registers a dynamic selection resolver.
* The resolver is added to the sorted list of selection resolvers and can later be disposed to remove it.
*
* @param resolver The selection resolver to register.
* @returns A Disposable that unregisters the resolver when disposed.
*/
public registerSelectionResolver(resolver: EditorSelectionResolver): Disposable {
this.selectionResolvers.push(resolver);
this.selectionResolvers.sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
return {
dispose: () => {
const index = this.selectionResolvers.indexOf(resolver);
if (index !== -1) {
this.selectionResolvers.splice(index, 1);
}
}
};
}
override getByUri(uri: URI, options?: EditorOpenerOptions): Promise<EditorWidget | undefined> {
return this.getWidget(uri, options);
}
override getOrCreateByUri(uri: URI, options?: EditorOpenerOptions): Promise<EditorWidget> {
return this.getOrCreateWidget(uri, options);
}
createByUri(uri: URI, options?: EditorOpenerOptions): Promise<EditorWidget> {
const counter = this.createCounterForUri(uri);
if (!options?.counter || options.counter < counter) {
options = { ...options, counter };
}
return this.getOrCreateByUri(uri, options);
}
protected readonly recentlyVisibleIds: string[] = [];
protected get recentlyVisible(): EditorWidget | undefined {
const id = this.recentlyVisibleIds[0];
return id && this.all.find(w => w.id === id) || undefined;
}
protected addRecentlyVisible(widget: EditorWidget): void {
this.removeRecentlyVisible(widget);
this.recentlyVisibleIds.unshift(widget.id);
}
protected removeRecentlyVisible(widget: EditorWidget): void {
const index = this.recentlyVisibleIds.indexOf(widget.id);
if (index !== -1) {
this.recentlyVisibleIds.splice(index, 1);
}
}
protected _activeEditor: EditorWidget | undefined;
/**
* The active editor.
* If there is an active editor (one that has focus), active and current are the same.
*/
get activeEditor(): EditorWidget | undefined {
return this._activeEditor;
}
protected setActiveEditor(active: EditorWidget | undefined): void {
if (this._activeEditor !== active) {
this._activeEditor = active;
this.onActiveEditorChangedEmitter.fire(this._activeEditor);
}
}
protected updateActiveEditor(): void {
const widget = this.shell.activeWidget;
if (widget instanceof EditorWidget) {
this.addRecentlyVisible(widget);
this.setActiveEditor(widget);
} else {
this.setActiveEditor(undefined);
}
}
protected _currentEditor: EditorWidget | undefined;
/**
* The most recently activated editor (which might not have the focus anymore, hence it is not active).
* If no editor has focus, e.g. when a context menu is shown, the active editor is `undefined`, but current might be the editor that was active before the menu popped up.
*/
get currentEditor(): EditorWidget | undefined {
return this._currentEditor;
}
protected setCurrentEditor(current: EditorWidget | undefined): void {
if (this._currentEditor !== current) {
this._currentEditor = current;
this.onCurrentEditorChangedEmitter.fire(this._currentEditor);
}
}
protected updateCurrentEditor(): void {
const widget = this.shell.currentWidget;
if (widget instanceof EditorWidget) {
this.setCurrentEditor(widget);
} else if (!this._currentEditor || !this._currentEditor.isVisible || this.currentEditor !== this.recentlyVisible) {
this.setCurrentEditor(this.recentlyVisible);
}
}
canHandle(uri: URI, options?: WidgetOpenerOptions): number {
if (DiffUris.isDiffUri(uri)) {
const [/* left */, right] = DiffUris.decode(uri);
uri = right;
}
if (getDefaultHandler(uri, this.preferenceService) === 'default') {
return defaultHandlerPriority;
}
return 100;
}
override async open(uri: URI, options?: EditorOpenerOptions): Promise<EditorWidget> {
this.navigationLocationService.startNavigation();
try {
if (options?.counter === undefined) {
const insertionOptions = this.shell.getInsertionOptions(options?.widgetOptions);
// Definitely creating a new tabbar - no widget can match.
if (insertionOptions.addOptions.mode?.startsWith('split')) {
return await super.open(uri, { counter: this.createCounterForUri(uri), ...options });
}
// Check the target tabbar for an existing widget.
const tabbar = insertionOptions.addOptions.ref && this.shell.getTabBarFor(insertionOptions.addOptions.ref);
if (tabbar) {
const currentUri = uri.toString();
for (const title of tabbar.titles) {
if (title.owner instanceof EditorWidget) {
const { uri: otherWidgetUri, id } = this.extractIdFromWidget(title.owner);
if (otherWidgetUri === currentUri) {
return await super.open(uri, { counter: id, ...options });
}
}
}
}
// If the user has opted to prefer to open an existing editor even if it's on a different tab, check if we have anything about the URI.
if (this.preferenceService.get('workbench.editor.revealIfOpen', false)) {
const counter = this.getCounterForUri(uri);
if (counter !== undefined) {
return await super.open(uri, { counter, ...options });
}
}
// Open a new widget.
return await super.open(uri, { counter: this.createCounterForUri(uri), ...options });
}
return await super.open(uri, options);
} finally {
this.navigationLocationService.endNavigation();
}
}
/**
* Opens an editor to the side of the current editor. Defaults to opening to the right.
* To modify direction, pass options with `{widgetOptions: {mode: ...}}`
*/
openToSide(uri: URI, options?: EditorOpenerOptions): Promise<EditorWidget> {
const counter = this.createCounterForUri(uri);
const splitOptions: EditorOpenerOptions = { widgetOptions: { mode: 'split-right' }, ...options, counter };
return this.open(uri, splitOptions);
}
protected override async doOpen(widget: EditorWidget, uri: URI, options?: EditorOpenerOptions): Promise<void> {
await super.doOpen(widget, uri, options);
await this.revealSelection(widget, uri, options);
}
protected async revealSelection(widget: EditorWidget, uri: URI, options?: EditorOpenerOptions): Promise<void> {
let inputSelection = options?.selection;
if (!inputSelection) {
inputSelection = await this.resolveSelection(widget, options ?? {}, uri);
}
// this logic could be moved into a 'EditorSelectionResolver'
if (!inputSelection && uri) {
// support file:///some/file.js#73,84
// support file:///some/file.js#L73
const match = /^L?(\d+)(?:,(\d+))?/.exec(uri.fragment);
if (match) {
inputSelection = {
start: {
line: parseInt(match[1]) - 1,
character: match[2] ? parseInt(match[2]) - 1 : 0
}
};
}
}
if (inputSelection) {
const selection = this.getSelection(widget, inputSelection);
const editor = widget.editor;
if (Position.is(selection)) {
editor.cursor = selection;
editor.revealPosition(selection, { vertical: options?.revealOption ?? 'center' });
} else if (Range.is(selection)) {
editor.cursor = selection.end;
editor.selection = { ...selection, direction: 'ltr' };
editor.revealRange(selection, { at: options?.revealOption ?? 'center' });
}
}
}
protected async resolveSelection(widget: EditorWidget, options: EditorOpenerOptions, uri?: URI): Promise<RecursivePartial<Range> | undefined> {
if (options.selection) {
return options.selection;
}
for (const resolver of this.selectionResolvers) {
try {
const selection = await resolver.resolveSelection(widget, options, uri);
if (selection) {
return selection;
}
} catch (error) {
console.error(error);
}
}
return undefined;
}
protected getSelection(widget: EditorWidget, selection: RecursivePartial<Range>): Range | Position | undefined {
const { start, end } = selection;
if (Position.is(start)) {
if (Position.is(end)) {
return widget.editor.document.toValidRange({ start, end });
}
return widget.editor.document.toValidPosition(start);
}
const line = start && start.line !== undefined && start.line >= 0 ? start.line : undefined;
if (line === undefined) {
return undefined;
}
const character = start && start.character !== undefined && start.character >= 0 ? start.character : widget.editor.document.getLineMaxColumn(line);
const endLine = end && end.line !== undefined && end.line >= 0 ? end.line : undefined;
if (endLine === undefined) {
return { line, character };
}
const endCharacter = end && end.character !== undefined && end.character >= 0 ? end.character : widget.editor.document.getLineMaxColumn(endLine);
return {
start: { line, character },
end: { line: endLine, character: endCharacter }
};
}
protected removeFromCounter(widget: EditorWidget): void {
const { id, uri } = this.extractIdFromWidget(widget);
if (uri && !Number.isNaN(id)) {
let max = -Infinity;
this.all.forEach(editor => {
const candidateID = this.extractIdFromWidget(editor);
if ((candidateID.uri === uri) && (candidateID.id > max)) {
max = candidateID.id!;
}
});
if (max > -Infinity) {
this.editorCounters.set(uri, max);
} else {
this.editorCounters.delete(uri);
}
}
}
protected extractIdFromWidget(widget: EditorWidget): WidgetId {
const uri = widget.editor.uri.toString();
const id = Number(widget.id.slice(widget.id.lastIndexOf(':') + 1));
return { id, uri };
}
protected checkCounterForWidget(widget: EditorWidget): void {
const { id, uri } = this.extractIdFromWidget(widget);
const numericalId = Number(id);
if (uri && !Number.isNaN(numericalId)) {
const highestKnownId = this.editorCounters.get(uri) ?? -Infinity;
if (numericalId > highestKnownId) {
this.editorCounters.set(uri, numericalId);
}
}
}
protected createCounterForUri(uri: URI): number {
const identifier = uri.toString();
const next = (this.editorCounters.get(identifier) ?? 0) + 1;
return next;
}
protected getCounterForUri(uri: URI): number | undefined {
const idWithoutCounter = EditorWidgetFactory.createID(uri);
const counterOfMostRecentlyVisibleEditor = this.recentlyVisibleIds.find(id => id.startsWith(idWithoutCounter))?.slice(idWithoutCounter.length + 1);
return counterOfMostRecentlyVisibleEditor === undefined ? undefined : parseInt(counterOfMostRecentlyVisibleEditor);
}
protected getOrCreateCounterForUri(uri: URI): number {
return this.getCounterForUri(uri) ?? this.createCounterForUri(uri);
}
protected override createWidgetOptions(uri: URI, options?: EditorOpenerOptions): NavigatableWidgetOptions {
const navigatableOptions = super.createWidgetOptions(uri, options);
navigatableOptions.counter = options?.counter ?? this.getOrCreateCounterForUri(uri);
return navigatableOptions;
}
}
/**
* Provides direct access to the underlying text editor.
*/
@injectable()
export abstract class EditorAccess {
@inject(EditorManager)
protected readonly editorManager: EditorManager;
/**
* The URI of the underlying document from the editor.
*/
get uri(): string | undefined {
const editor = this.editor;
if (editor) {
return editor.uri.toString();
}
return undefined;
}
/**
* The selection location from the text editor.
*/
get selection(): Location | undefined {
const editor = this.editor;
if (editor) {
const uri = editor.uri.toString();
const range = editor.selection;
return {
range,
uri
};
}
return undefined;
}
/**
* The unique identifier of the language the current editor belongs to.
*/
get languageId(): string | undefined {
const editor = this.editor;
if (editor) {
return editor.document.languageId;
}
return undefined;
}
/**
* The text editor.
*/
get editor(): TextEditor | undefined {
const editorWidget = this.editorWidget();
if (editorWidget) {
return editorWidget.editor;
}
return undefined;
}
/**
* The editor widget, or `undefined` if not applicable.
*/
protected abstract editorWidget(): EditorWidget | undefined;
}
/**
* Provides direct access to the currently active text editor.
*/
@injectable()
export class CurrentEditorAccess extends EditorAccess {
protected editorWidget(): EditorWidget | undefined {
return this.editorManager.currentEditor;
}
}
/**
* Provides access to the active text editor.
*/
@injectable()
export class ActiveEditorAccess extends EditorAccess {
protected editorWidget(): EditorWidget | undefined {
return this.editorManager.activeEditor;
}
}
export namespace EditorAccess {
export const CURRENT = 'current-editor-access';
export const ACTIVE = 'active-editor-access';
}

View File

@@ -0,0 +1,227 @@
// *****************************************************************************
// Copyright (C) 2017 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { injectable } from '@theia/core/shared/inversify';
import { MenuContribution, MenuModelRegistry, MenuPath, MAIN_MENU_BAR } from '@theia/core';
import { CommonCommands, CommonMenus } from '@theia/core/lib/browser';
import { EditorCommands } from './editor-command';
import { nls } from '@theia/core/lib/common/nls';
export const EDITOR_CONTEXT_MENU: MenuPath = ['editor_context_menu'];
/** Corresponds to `editor/content` contribution point in VS Code. */
export const EDITOR_CONTENT_MENU: MenuPath = ['editor_content_menu'];
/**
* Editor context menu default groups should be aligned
* with VS Code default groups: https://code.visualstudio.com/api/references/contribution-points#contributes.menus
*/
export namespace EditorContextMenu {
export const NAVIGATION = [...EDITOR_CONTEXT_MENU, 'navigation'];
export const MODIFICATION = [...EDITOR_CONTEXT_MENU, '1_modification'];
export const CUT_COPY_PASTE = [...EDITOR_CONTEXT_MENU, '9_cutcopypaste'];
export const COMMANDS = [...EDITOR_CONTEXT_MENU, 'z_commands'];
export const UNDO_REDO = [...EDITOR_CONTEXT_MENU, '1_undo'];
}
export namespace EditorMainMenu {
/**
* The main `Go` menu item.
*/
export const GO = [...MAIN_MENU_BAR, '5_go'];
/**
* Navigation menu group in the `Go` main-menu.
*/
export const NAVIGATION_GROUP = [...GO, '1_navigation_group'];
/**
* Context management group in the `Go` main menu: Pane and editor switching commands.
*/
export const CONTEXT_GROUP = [...GO, '1.1_context_group'];
/**
* Submenu for switching panes in the main area.
*/
export const PANE_GROUP = [...CONTEXT_GROUP, '2_pane_group'];
export const BY_NUMBER = [...EditorMainMenu.PANE_GROUP, '1_by_number'];
export const NEXT_PREVIOUS = [...EditorMainMenu.PANE_GROUP, '2_by_location'];
/**
* Workspace menu group in the `Go` main-menu.
*/
export const WORKSPACE_GROUP = [...GO, '2_workspace_group'];
/**
* Language features menu group in the `Go` main-menu.
*/
export const LANGUAGE_FEATURES_GROUP = [...GO, '3_language_features_group'];
/**
* Location menu group in the `Go` main-menu.
*/
export const LOCATION_GROUP = [...GO, '4_locations'];
}
@injectable()
export class EditorMenuContribution implements MenuContribution {
registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(EditorContextMenu.UNDO_REDO, {
commandId: CommonCommands.UNDO.id
});
registry.registerMenuAction(EditorContextMenu.UNDO_REDO, {
commandId: CommonCommands.REDO.id
});
registry.registerMenuAction(EditorContextMenu.CUT_COPY_PASTE, {
commandId: CommonCommands.CUT.id,
order: '0'
});
registry.registerMenuAction(EditorContextMenu.CUT_COPY_PASTE, {
commandId: CommonCommands.COPY.id,
order: '1'
});
registry.registerMenuAction(EditorContextMenu.CUT_COPY_PASTE, {
commandId: CommonCommands.PASTE.id,
order: '2'
});
// Editor navigation. Go > Back and Go > Forward.
registry.registerSubmenu(EditorMainMenu.GO, nls.localizeByDefault('Go'));
registry.registerMenuAction(EditorMainMenu.NAVIGATION_GROUP, {
commandId: EditorCommands.GO_BACK.id,
label: EditorCommands.GO_BACK.label,
order: '1'
});
registry.registerMenuAction(EditorMainMenu.NAVIGATION_GROUP, {
commandId: EditorCommands.GO_FORWARD.id,
label: EditorCommands.GO_FORWARD.label,
order: '2'
});
registry.registerMenuAction(EditorMainMenu.NAVIGATION_GROUP, {
commandId: EditorCommands.GO_LAST_EDIT.id,
label: nls.localizeByDefault('Last Edit Location'),
order: '3'
});
registry.registerSubmenu(EditorMainMenu.PANE_GROUP, nls.localizeByDefault('Switch Group'));
registry.registerMenuAction(EditorMainMenu.BY_NUMBER, {
commandId: 'workbench.action.focusFirstEditorGroup',
label: nls.localizeByDefault('Group 1'),
});
registry.registerMenuAction(EditorMainMenu.BY_NUMBER, {
commandId: 'workbench.action.focusSecondEditorGroup',
label: nls.localizeByDefault('Group 2'),
});
registry.registerMenuAction(EditorMainMenu.BY_NUMBER, {
commandId: 'workbench.action.focusThirdEditorGroup',
label: nls.localizeByDefault('Group 3'),
});
registry.registerMenuAction(EditorMainMenu.BY_NUMBER, {
commandId: 'workbench.action.focusFourthEditorGroup',
label: nls.localizeByDefault('Group 4'),
});
registry.registerMenuAction(EditorMainMenu.BY_NUMBER, {
commandId: 'workbench.action.focusFifthEditorGroup',
label: nls.localizeByDefault('Group 5'),
});
registry.registerMenuAction(EditorMainMenu.NEXT_PREVIOUS, {
commandId: CommonCommands.NEXT_TAB_GROUP.id,
label: nls.localizeByDefault('Next Group'),
order: '1'
});
registry.registerMenuAction(EditorMainMenu.NEXT_PREVIOUS, {
commandId: CommonCommands.PREVIOUS_TAB_GROUP.id,
label: nls.localizeByDefault('Previous Group'),
order: '2'
});
registry.registerMenuAction(EditorMainMenu.LOCATION_GROUP, {
commandId: EditorCommands.GOTO_LINE_COLUMN.id,
order: '1'
});
// Toggle Commands.
registry.registerMenuAction(CommonMenus.VIEW_TOGGLE, {
commandId: EditorCommands.TOGGLE_WORD_WRAP.id,
order: '0'
});
registry.registerMenuAction(CommonMenus.VIEW_TOGGLE, {
commandId: EditorCommands.TOGGLE_MINIMAP.id,
order: '1',
});
registry.registerMenuAction(CommonMenus.VIEW_TOGGLE, {
commandId: CommonCommands.TOGGLE_BREADCRUMBS.id,
order: '2',
});
registry.registerMenuAction(CommonMenus.VIEW_TOGGLE, {
commandId: EditorCommands.TOGGLE_RENDER_WHITESPACE.id,
order: '3'
});
registry.registerMenuAction(CommonMenus.VIEW_TOGGLE, {
commandId: EditorCommands.TOGGLE_STICKY_SCROLL.id,
order: '4'
});
registry.registerMenuAction(CommonMenus.FILE_CLOSE, {
commandId: CommonCommands.CLOSE_MAIN_TAB.id,
label: nls.localizeByDefault('Close Editor'),
order: '1'
});
registry.registerMenuAction(CommonMenus.VIEW_EDITOR_SUBMENU_SPLIT, {
commandId: EditorCommands.SPLIT_EDITOR_RIGHT.id,
label: nls.localizeByDefault('Split Editor Right'),
order: '0'
});
registry.registerMenuAction(CommonMenus.VIEW_EDITOR_SUBMENU_SPLIT, {
commandId: EditorCommands.SPLIT_EDITOR_LEFT.id,
label: nls.localizeByDefault('Split Editor Left'),
order: '1'
});
registry.registerMenuAction(CommonMenus.VIEW_EDITOR_SUBMENU_SPLIT, {
commandId: EditorCommands.SPLIT_EDITOR_UP.id,
label: nls.localizeByDefault('Split Editor Up'),
order: '2'
});
registry.registerMenuAction(CommonMenus.VIEW_EDITOR_SUBMENU_SPLIT, {
commandId: EditorCommands.SPLIT_EDITOR_DOWN.id,
label: nls.localizeByDefault('Split Editor Down'),
order: '3'
});
registry.registerMenuAction(CommonMenus.VIEW_EDITOR_SUBMENU_ORTHO, {
commandId: EditorCommands.SPLIT_EDITOR_HORIZONTAL.id,
label: nls.localize('theia/editor/splitHorizontal', 'Split Editor Horizontal'),
order: '1'
});
registry.registerMenuAction(CommonMenus.VIEW_EDITOR_SUBMENU_ORTHO, {
commandId: EditorCommands.SPLIT_EDITOR_VERTICAL.id,
label: nls.localize('theia/editor/splitVertical', 'Split Editor Vertical'),
order: '2'
});
registry.registerSubmenu(CommonMenus.VIEW_EDITOR_SUBMENU, nls.localizeByDefault('Editor Layout'));
}
}

View File

@@ -0,0 +1,332 @@
// *****************************************************************************
// Copyright (C) 2018 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
import { ILogger } from '@theia/core/lib/common/logger';
import { StorageService } from '@theia/core/lib/browser/storage-service';
import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable';
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application-contribution';
import { CommandRegistry } from '@theia/core/lib/common/command';
import { EditorCommands } from './editor-command';
import { EditorWidget } from './editor-widget';
import { EditorManager } from './editor-manager';
import { TextEditor, Position, Range, TextDocumentChangeEvent } from './editor';
import { NavigationLocation, RecentlyClosedEditor } from './navigation/navigation-location';
import { NavigationLocationService } from './navigation/navigation-location-service';
import { addEventListener } from '@theia/core/lib/browser';
import { ConfirmDialog, Dialog } from '@theia/core/lib/browser/dialogs';
import { nls } from '@theia/core';
import { PreferenceService, PreferenceScope } from '@theia/core/lib/common/preferences';
@injectable()
export class EditorNavigationContribution implements Disposable, FrontendApplicationContribution {
private static ID = 'editor-navigation-contribution';
private static CLOSED_EDITORS_KEY = 'recently-closed-editors';
private static MOUSE_NAVIGATION_PREFERENCE = 'workbench.editor.mouseBackForwardToNavigate';
protected readonly toDispose = new DisposableCollection();
protected readonly toDisposePerCurrentEditor = new DisposableCollection();
@inject(ILogger)
protected readonly logger: ILogger;
@inject(EditorManager)
protected readonly editorManager: EditorManager;
@inject(NavigationLocationService)
protected readonly locationStack: NavigationLocationService;
@inject(StorageService)
protected readonly storageService: StorageService;
@inject(PreferenceService)
protected readonly preferenceService: PreferenceService;
@inject(CommandRegistry)
protected readonly commandRegistry: CommandRegistry;
@postConstruct()
protected init(): void {
this.toDispose.pushAll([
// TODO listen on file resource changes, if a file gets deleted, remove the corresponding navigation locations (if any).
// This would require introducing the FS dependency in the editor extension.
this.editorManager.onCurrentEditorChanged(this.onCurrentEditorChanged.bind(this)),
this.editorManager.onCreated(widget => {
this.locationStack.removeClosedEditor(widget.editor.uri);
widget.disposed.connect(() => this.locationStack.addClosedEditor({
uri: widget.editor.uri,
viewState: widget.editor.storeViewState()
}));
})
]);
this.commandRegistry.registerHandler(EditorCommands.GO_BACK.id, {
execute: () => this.locationStack.back(),
isEnabled: () => this.locationStack.canGoBack()
});
this.commandRegistry.registerHandler(EditorCommands.GO_FORWARD.id, {
execute: () => this.locationStack.forward(),
isEnabled: () => this.locationStack.canGoForward()
});
this.commandRegistry.registerHandler(EditorCommands.GO_LAST_EDIT.id, {
execute: () => this.locationStack.reveal(this.locationStack.lastEditLocation()),
isEnabled: () => !!this.locationStack.lastEditLocation()
});
this.commandRegistry.registerHandler(EditorCommands.CLEAR_EDITOR_HISTORY.id, {
execute: async () => {
const shouldClear = await new ConfirmDialog({
title: nls.localizeByDefault('Clear Editor History'),
msg: nls.localizeByDefault('Do you want to clear the history of recently opened editors?'),
ok: Dialog.YES,
cancel: Dialog.NO
}).open();
if (shouldClear) {
this.locationStack.clearHistory();
}
},
isEnabled: () => this.locationStack.locations().length > 0
});
this.commandRegistry.registerHandler(EditorCommands.TOGGLE_MINIMAP.id, {
execute: () => this.toggleMinimap(),
isEnabled: () => true,
isToggled: () => this.isMinimapEnabled()
});
this.commandRegistry.registerHandler(EditorCommands.TOGGLE_RENDER_WHITESPACE.id, {
execute: () => this.toggleRenderWhitespace(),
isEnabled: () => true,
isToggled: () => this.isRenderWhitespaceEnabled()
});
this.commandRegistry.registerHandler(EditorCommands.TOGGLE_WORD_WRAP.id, {
execute: () => this.toggleWordWrap(),
isEnabled: () => true,
});
this.commandRegistry.registerHandler(EditorCommands.TOGGLE_STICKY_SCROLL.id, {
execute: () => this.toggleStickyScroll(),
isEnabled: () => true,
isToggled: () => this.isStickyScrollEnabled()
});
this.commandRegistry.registerHandler(EditorCommands.REOPEN_CLOSED_EDITOR.id, {
execute: () => this.reopenLastClosedEditor()
});
this.installMouseNavigationSupport();
}
protected async installMouseNavigationSupport(): Promise<void> {
const mouseNavigationSupport = new DisposableCollection();
const updateMouseNavigationListener = () => {
mouseNavigationSupport.dispose();
if (this.shouldNavigateWithMouse()) {
mouseNavigationSupport.push(addEventListener(document.body, 'mousedown', event => this.onMouseDown(event), true));
}
};
this.toDispose.push(this.preferenceService.onPreferenceChanged(change => {
if (change.preferenceName === EditorNavigationContribution.MOUSE_NAVIGATION_PREFERENCE) {
updateMouseNavigationListener();
}
}));
updateMouseNavigationListener();
this.toDispose.push(mouseNavigationSupport);
}
protected async onMouseDown(event: MouseEvent): Promise<void> {
// Support navigation in history when mouse buttons 4/5 are pressed
switch (event.button) {
case 3:
event.preventDefault();
this.locationStack.back();
break;
case 4:
event.preventDefault();
this.locationStack.forward();
break;
}
}
/**
* Reopens the last closed editor with its stored view state if possible from history.
* If the editor cannot be restored, continue to the next editor in history.
*/
protected async reopenLastClosedEditor(): Promise<void> {
const lastClosedEditor = this.locationStack.getLastClosedEditor();
if (lastClosedEditor === undefined) {
return;
}
try {
const widget = await this.editorManager.open(lastClosedEditor.uri);
widget.editor.restoreViewState(lastClosedEditor.viewState);
} catch {
this.locationStack.removeClosedEditor(lastClosedEditor.uri);
this.reopenLastClosedEditor();
}
}
async onStart(): Promise<void> {
this.restoreState();
}
onStop(): void {
this.storeState();
this.dispose();
}
dispose(): void {
this.toDispose.dispose();
}
/**
* Toggle the editor word wrap behavior.
*/
protected async toggleWordWrap(): Promise<void> {
// Get the current word wrap.
const wordWrap: string | undefined = this.preferenceService.get('editor.wordWrap');
if (wordWrap === undefined) {
return;
}
// The list of allowed word wrap values.
const values: string[] = ['off', 'on', 'wordWrapColumn', 'bounded'];
// Get the index of the current value, and toggle to the next available value.
const index = values.indexOf(wordWrap) + 1;
if (index > -1) {
await this.preferenceService.set('editor.wordWrap', values[index % values.length], PreferenceScope.User);
}
}
/**
* Toggle the display of sticky scroll in the editor.
*/
protected async toggleStickyScroll(): Promise<void> {
const value: boolean | undefined = this.preferenceService.get('editor.stickyScroll.enabled');
await this.preferenceService.set('editor.stickyScroll.enabled', !value, PreferenceScope.User);
}
/**
* Toggle the display of minimap in the editor.
*/
protected async toggleMinimap(): Promise<void> {
const value: boolean | undefined = this.preferenceService.get('editor.minimap.enabled');
await this.preferenceService.set('editor.minimap.enabled', !value, PreferenceScope.User);
}
/**
* Toggle the rendering of whitespace in the editor.
*/
protected async toggleRenderWhitespace(): Promise<void> {
const renderWhitespace: string | undefined = this.preferenceService.get('editor.renderWhitespace');
let updatedRenderWhitespace: string;
if (renderWhitespace === 'none') {
updatedRenderWhitespace = 'all';
} else {
updatedRenderWhitespace = 'none';
}
await this.preferenceService.set('editor.renderWhitespace', updatedRenderWhitespace, PreferenceScope.User);
}
protected onCurrentEditorChanged(editorWidget: EditorWidget | undefined): void {
this.toDisposePerCurrentEditor.dispose();
if (editorWidget) {
const { editor } = editorWidget;
this.toDisposePerCurrentEditor.pushAll([
// Instead of registering an `onCursorPositionChanged` listener, we treat the zero length selection as a cursor position change.
// Otherwise we would have two events for a single cursor change interaction.
editor.onSelectionChanged(selection => this.onSelectionChanged(editor, selection)),
editor.onDocumentContentChanged(event => this.onDocumentContentChanged(editor, event))
]);
this.locationStack.navigate(service => service.register(NavigationLocation.create(editor, editor.selection)));
}
}
protected onCursorPositionChanged(editor: TextEditor, position: Position): void {
this.locationStack.navigate(service => service.register(NavigationLocation.create(editor, position)));
}
protected onSelectionChanged(editor: TextEditor, selection: Range): void {
if (this.isZeroLengthRange(selection)) {
this.onCursorPositionChanged(editor, selection.start);
} else {
this.locationStack.navigate(service => service.register(NavigationLocation.create(editor, selection)));
}
}
protected onDocumentContentChanged(editor: TextEditor, event: TextDocumentChangeEvent): void {
if (event.contentChanges.length > 0) {
this.locationStack.navigate(service => service.register(NavigationLocation.create(editor, event.contentChanges[0])));
}
}
/**
* `true` if the `range` argument has zero length. In other words, the `start` and the `end` positions are the same. Otherwise, `false`.
*/
protected isZeroLengthRange(range: Range): boolean {
const { start, end } = range;
return start.line === end.line && start.character === end.character;
}
protected async storeState(): Promise<void> {
this.storageService.setData(EditorNavigationContribution.ID, this.locationStack.storeState());
this.storageService.setData(EditorNavigationContribution.CLOSED_EDITORS_KEY, {
closedEditors: this.shouldStoreClosedEditors() ? this.locationStack.closedEditorsStack.map(RecentlyClosedEditor.toObject) : []
});
}
protected async restoreState(): Promise<void> {
await this.restoreNavigationLocations();
await this.restoreClosedEditors();
}
protected async restoreNavigationLocations(): Promise<void> {
const raw = await this.storageService.getData(EditorNavigationContribution.ID);
if (raw && typeof raw === 'object') {
this.locationStack.restoreState(raw);
}
}
protected async restoreClosedEditors(): Promise<void> {
const raw: { closedEditors?: ArrayLike<object> } | undefined = await this.storageService.getData(EditorNavigationContribution.CLOSED_EDITORS_KEY);
if (raw && raw.closedEditors) {
for (let i = 0; i < raw.closedEditors.length; i++) {
const editor = RecentlyClosedEditor.fromObject(raw.closedEditors[i]);
if (editor) {
this.locationStack.addClosedEditor(editor);
} else {
this.logger.warn('Could not restore the state of the closed editors stack.');
}
}
}
}
private isMinimapEnabled(): boolean {
return !!this.preferenceService.get('editor.minimap.enabled');
}
private isRenderWhitespaceEnabled(): boolean {
const renderWhitespace = this.preferenceService.get('editor.renderWhitespace');
return renderWhitespace === 'none' ? false : true;
}
private shouldStoreClosedEditors(): boolean {
return !!this.preferenceService.get('editor.history.persistClosedEditors');
}
private shouldNavigateWithMouse(): boolean {
return !!this.preferenceService.get(EditorNavigationContribution.MOUSE_NAVIGATION_PREFERENCE);
}
private isStickyScrollEnabled(): boolean {
return !!this.preferenceService.get('editor.stickyScroll.enabled');
}
}

View File

@@ -0,0 +1,62 @@
// *****************************************************************************
// Copyright (C) 2018 Red Hat, Inc. and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { injectable, inject } from '@theia/core/shared/inversify';
import { VariableRegistry, VariableContribution } from '@theia/variable-resolver/lib/browser';
import { TextEditor } from './editor';
import { EditorManager } from './editor-manager';
@injectable()
export class EditorVariableContribution implements VariableContribution {
@inject(EditorManager)
protected readonly editorManager: EditorManager;
registerVariables(variables: VariableRegistry): void {
variables.registerVariable({
name: 'lineNumber',
description: 'The current line number in the currently opened file',
resolve: () => {
const editor = this.getCurrentEditor();
return editor ? `${editor.cursor.line + 1}` : undefined;
}
});
variables.registerVariable({
name: 'selectedText',
description: 'The current selected text in the active file',
resolve: () => {
const editor = this.getCurrentEditor();
return editor?.document.getText(editor.selection);
}
});
variables.registerVariable({
name: 'currentText',
description: 'The current text in the active file',
resolve: () => {
const editor = this.getCurrentEditor();
return editor?.document.getText();
}
});
}
protected getCurrentEditor(): TextEditor | undefined {
const currentEditor = this.editorManager.currentEditor;
if (!currentEditor) {
return undefined;
}
return currentEditor.editor;
}
}

View File

@@ -0,0 +1,82 @@
// *****************************************************************************
// Copyright (C) 2018 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { injectable, inject } from '@theia/core/shared/inversify';
import URI from '@theia/core/lib/common/uri';
import { nls, SelectionService } from '@theia/core/lib/common';
import { NavigatableWidgetOptions, WidgetFactory, LabelProvider } from '@theia/core/lib/browser';
import { EditorWidget } from './editor-widget';
import { TextEditorProvider } from './editor';
@injectable()
export class EditorWidgetFactory implements WidgetFactory {
static createID(uri: URI, counter?: number): string {
return EditorWidgetFactory.ID
+ `:${uri.toString()}`
+ (counter !== undefined ? `:${counter}` : '');
}
static ID = 'code-editor-opener';
readonly id = EditorWidgetFactory.ID;
@inject(LabelProvider)
protected readonly labelProvider: LabelProvider;
@inject(TextEditorProvider)
protected readonly editorProvider: TextEditorProvider;
@inject(SelectionService)
protected readonly selectionService: SelectionService;
createWidget(options: NavigatableWidgetOptions): Promise<EditorWidget> {
const uri = new URI(options.uri);
return this.createEditor(uri, options);
}
protected async createEditor(uri: URI, options?: NavigatableWidgetOptions): Promise<EditorWidget> {
const newEditor = await this.constructEditor(uri);
this.setLabels(newEditor, uri);
const labelListener = this.labelProvider.onDidChange(event => {
if (event.affects(uri)) {
this.setLabels(newEditor, uri);
}
});
newEditor.onDispose(() => labelListener.dispose());
newEditor.id = EditorWidgetFactory.createID(uri, options?.counter);
newEditor.title.closable = true;
return newEditor;
}
protected async constructEditor(uri: URI): Promise<EditorWidget> {
const textEditor = await this.editorProvider(uri);
return new EditorWidget(textEditor, this.selectionService);
}
private setLabels(editor: EditorWidget, uri: URI): void {
editor.title.caption = uri.path.fsPath();
if (editor.editor.isReadonly) {
editor.title.caption += `${nls.localizeByDefault('Read-only')}`;
}
const icon = this.labelProvider.getIcon(uri);
editor.title.label = this.labelProvider.getName(uri);
editor.title.iconClass = icon + ' file-icon';
}
}

View File

@@ -0,0 +1,146 @@
// *****************************************************************************
// Copyright (C) 2017 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { Disposable, SelectionService, Event, UNTITLED_SCHEME, DisposableCollection } from '@theia/core/lib/common';
import { Widget, BaseWidget, Message, Saveable, SaveableSource, Navigatable, StatefulWidget, lock, TabBar, DockPanel, unlock, ExtractableWidget } from '@theia/core/lib/browser';
import URI from '@theia/core/lib/common/uri';
import { find } from '@theia/core/shared/@lumino/algorithm';
import { TextEditor } from './editor';
export class EditorWidget extends BaseWidget implements SaveableSource, Navigatable, StatefulWidget, ExtractableWidget {
protected toDisposeOnTabbarChange = new DisposableCollection();
protected currentTabbar: TabBar<Widget> | undefined;
constructor(
readonly editor: TextEditor,
protected readonly selectionService: SelectionService
) {
super(editor);
this.addClass('theia-editor');
if (editor.isReadonly) {
lock(this.title);
}
this.toDispose.push(this.editor);
this.toDispose.push(this.editor.onSelectionChanged(() => this.setSelection()));
this.toDispose.push(this.editor.onFocusChanged(() => this.setSelection()));
this.toDispose.push(this.editor.onDidChangeReadOnly(isReadonly => {
if (isReadonly) {
lock(this.title);
} else {
unlock(this.title);
}
}));
this.toDispose.push(Disposable.create(() => {
this.toDisposeOnTabbarChange.dispose();
}));
this.toDispose.push(Disposable.create(() => {
if (this.selectionService.selection === this.editor) {
this.selectionService.selection = undefined;
}
}));
}
isExtractable: boolean = true;
secondaryWindow: Window | undefined;
setSelection(): void {
if (this.editor.isFocused() && this.selectionService.selection !== this.editor) {
this.selectionService.selection = this.editor;
}
}
protected override handleVisiblityChanged(isNowVisible: boolean): void {
this.editor.handleVisibilityChanged(isNowVisible);
super.handleVisiblityChanged(isNowVisible);
}
get saveable(): Saveable {
return this.editor.document;
}
getResourceUri(): URI | undefined {
return this.editor.getResourceUri();
}
createMoveToUri(resourceUri: URI): URI | undefined {
return this.editor.createMoveToUri(resourceUri);
}
protected override onActivateRequest(msg: Message): void {
super.onActivateRequest(msg);
this.editor.focus();
this.selectionService.selection = this.editor;
}
protected override onAfterAttach(msg: Message): void {
super.onAfterAttach(msg);
if (this.isVisible) {
this.editor.refresh();
}
this.checkForTabbarChange();
}
protected checkForTabbarChange(): void {
const { parent } = this;
if (parent instanceof DockPanel) {
const newTabbar = find(parent.tabBars(), tabbar => !!tabbar.titles.find(title => title === this.title));
if (this.currentTabbar !== newTabbar) {
this.toDisposeOnTabbarChange.dispose();
const listener = () => this.checkForTabbarChange();
parent.layoutModified.connect(listener);
this.toDisposeOnTabbarChange.push(Disposable.create(() => parent.layoutModified.disconnect(listener)));
const last = this.currentTabbar;
this.currentTabbar = newTabbar;
this.handleTabBarChange(last, newTabbar);
}
}
}
protected handleTabBarChange(oldTabBar?: TabBar<Widget>, newTabBar?: TabBar<Widget>): void {
const ownSaveable = Saveable.get(this);
const competingEditors = ownSaveable && newTabBar?.titles.filter(title => title !== this.title
&& (title.owner instanceof EditorWidget)
&& title.owner.editor.uri.isEqual(this.editor.uri)
&& Saveable.get(title.owner) === ownSaveable
);
competingEditors?.forEach(title => title.owner.close());
}
protected override onAfterShow(msg: Message): void {
super.onAfterShow(msg);
this.editor.refresh();
}
protected override onResize(msg: Widget.ResizeMessage): void {
if (msg.width < 0 || msg.height < 0) {
this.editor.resizeToFit();
} else {
this.editor.setSize(msg);
}
}
storeState(): object | undefined {
return this.getResourceUri()?.scheme === UNTITLED_SCHEME ? undefined : this.editor.storeViewState();
}
restoreState(oldState: object): void {
this.editor.restoreViewState(oldState);
}
get onDispose(): Event<void> {
return this.toDispose.onDispose;
}
}

View File

@@ -0,0 +1,378 @@
// *****************************************************************************
// Copyright (C) 2018 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { Position, Range, Location } from '@theia/core/shared/vscode-languageserver-protocol';
import * as lsp from '@theia/core/shared/vscode-languageserver-protocol';
import URI from '@theia/core/lib/common/uri';
import { Event, Disposable, TextDocumentContentChangeDelta, Reference, isObject } from '@theia/core/lib/common';
import { Saveable, Navigatable, Widget } from '@theia/core/lib/browser';
import { EditorDecoration } from './decorations/editor-decoration';
import { MarkdownString } from '@theia/core/lib/common/markdown-rendering';
export { Position, Range, Location };
export const TextEditorProvider = Symbol('TextEditorProvider');
export type TextEditorProvider = (uri: URI) => Promise<TextEditor>;
export interface TextEditorDocument extends lsp.TextDocument, Saveable, Disposable {
/**
* @param lineNumber 1-based
*/
getLineContent(lineNumber: number): string;
/**
* @param lineNumber 1-based
*/
getLineMaxColumn(lineNumber: number): number;
/**
* @since 1.8.0
*/
findMatches?(options: FindMatchesOptions): FindMatch[];
/**
* Creates a valid position. If the position is outside of the backing document, this method will return a position that is ensured to be inside the document and valid.
* For example, when the `position` is `{ line: 1, character: 0 }` and the document is empty, this method will return with `{ line: 0, character: 0 }`.
*/
toValidPosition(position: Position): Position;
/**
* Creates a valid range. If the `range` argument is outside of the document, this method will return with a new range that does not exceed the boundaries of the document.
* For example, if the argument is `{ start: { line: 1, character: 0 }, end: { line: 1, character: 0 } }` and the document is empty, the return value is
* `{ start: { line: 0, character: 0 }, end: { line: 0, character: 0 } }`.
*/
toValidRange(range: Range): Range;
}
// Refactoring
export { TextDocumentContentChangeDelta };
export interface TextDocumentChangeEvent {
readonly document: TextEditorDocument;
readonly contentChanges: TextDocumentContentChangeDelta[];
}
/**
* Type of hit element with the mouse in the editor.
* Copied from monaco editor.
*/
export enum MouseTargetType {
/**
* Mouse is on top of an unknown element.
*/
UNKNOWN = 0,
/**
* Mouse is on top of the textarea used for input.
*/
TEXTAREA = 1,
/**
* Mouse is on top of the glyph margin
*/
GUTTER_GLYPH_MARGIN = 2,
/**
* Mouse is on top of the line numbers
*/
GUTTER_LINE_NUMBERS = 3,
/**
* Mouse is on top of the line decorations
*/
GUTTER_LINE_DECORATIONS = 4,
/**
* Mouse is on top of the whitespace left in the gutter by a view zone.
*/
GUTTER_VIEW_ZONE = 5,
/**
* Mouse is on top of text in the content.
*/
CONTENT_TEXT = 6,
/**
* Mouse is on top of empty space in the content (e.g. after line text or below last line)
*/
CONTENT_EMPTY = 7,
/**
* Mouse is on top of a view zone in the content.
*/
CONTENT_VIEW_ZONE = 8,
/**
* Mouse is on top of a content widget.
*/
CONTENT_WIDGET = 9,
/**
* Mouse is on top of the decorations overview ruler.
*/
OVERVIEW_RULER = 10,
/**
* Mouse is on top of a scrollbar.
*/
SCROLLBAR = 11,
/**
* Mouse is on top of an overlay widget.
*/
OVERLAY_WIDGET = 12,
/**
* Mouse is outside of the editor.
*/
OUTSIDE_EDITOR = 13,
}
export interface MouseTarget {
/**
* The target element
*/
readonly element?: Element;
/**
* The target type
*/
readonly type: MouseTargetType;
/**
* The 'approximate' editor position
*/
readonly position?: Position;
/**
* Desired mouse column (e.g. when position.column gets clamped to text length -- clicking after text on a line).
*/
readonly mouseColumn: number;
/**
* The 'approximate' editor range
*/
readonly range?: Range;
/**
* Some extra detail.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
readonly detail: any;
}
export interface EditorMouseEvent {
readonly event: MouseEvent;
readonly target: MouseTarget;
}
export const enum EncodingMode {
/**
* Instructs the encoding support to encode the current input with the provided encoding
*/
Encode,
/**
* Instructs the encoding support to decode the current input with the provided encoding
*/
Decode
}
/**
* Options for searching in an editor.
*/
export interface FindMatchesOptions {
/**
* The string used to search. If it is a regular expression, set `isRegex` to true.
*/
searchString: string;
/**
* Used to indicate that `searchString` is a regular expression.
*/
isRegex: boolean;
/**
* Force the matching to match lower/upper case exactly.
*/
matchCase: boolean;
/**
* Force the matching to match entire words only.
*/
matchWholeWord: boolean;
/**
* Limit the number of results.
*/
limitResultCount?: number;
}
/**
* Representation of a find match.
*/
export interface FindMatch {
/**
* The textual match.
*/
readonly matches: string[];
/**
* The range for the given match.
*/
readonly range: Range;
}
export interface TextEditor extends Disposable, TextEditorSelection, Navigatable {
readonly node: HTMLElement;
readonly uri: URI;
readonly isReadonly: boolean | MarkdownString;
readonly onDidChangeReadOnly: Event<boolean | MarkdownString>;
readonly document: TextEditorDocument;
readonly onDocumentContentChanged: Event<TextDocumentChangeEvent>;
cursor: Position;
readonly onCursorPositionChanged: Event<Position>;
selection: Selection;
readonly onSelectionChanged: Event<Selection>;
/**
* The text editor should be revealed,
* otherwise it won't receive the focus.
*/
focus(): void;
blur(): void;
isFocused(): boolean;
readonly onFocusChanged: Event<boolean>;
readonly onMouseDown: Event<EditorMouseEvent>;
readonly onScrollChanged: Event<void>;
getVisibleRanges(): Range[];
revealPosition(position: Position, options?: RevealPositionOptions): void;
revealRange(range: Range, options?: RevealRangeOptions): void;
/**
* Rerender the editor.
*/
refresh(): void;
/**
* Resize the editor to fit its node.
*/
resizeToFit(): void;
setSize(size: Dimension): void;
/**
* Applies given new decorations, and removes old decorations identified by ids.
*
* @returns identifiers of applied decorations, which can be removed in next call.
*/
deltaDecorations(params: DeltaDecorationParams): string[];
/**
* Gets all the decorations for the lines between `startLineNumber` and `endLineNumber` as an array.
* @param startLineNumber The start line number.
* @param endLineNumber The end line number.
* @return An array with the decorations.
*/
getLinesDecorations(startLineNumber: number, endLineNumber: number): EditorDecoration[];
getVisibleColumn(position: Position): number;
/**
* Replaces the text of source given in ReplaceTextParams.
* @param params: ReplaceTextParams
*/
replaceText(params: ReplaceTextParams): Promise<boolean>;
/**
* Execute edits on the editor.
* @param edits: edits created with `lsp.TextEdit.replace`, `lsp.TextEdit.insert`, `lsp.TextEdit.del`
*/
executeEdits(edits: lsp.TextEdit[]): boolean;
storeViewState(): object;
restoreViewState(state: object): void;
detectLanguage(): void;
setLanguage(languageId: string): void;
readonly onLanguageChanged: Event<string>;
/**
* Gets the encoding of the input if known.
*/
getEncoding(): string;
/**
* Sets the encoding for the input for saving.
*/
setEncoding(encoding: string, mode: EncodingMode): void;
readonly onEncodingChanged: Event<string>;
shouldDisplayDirtyDiff(): boolean;
/**
* This event is optional iff {@link shouldDisplayDirtyDiff} always returns the same result for this editor instance.
*/
readonly onShouldDisplayDirtyDiffChanged?: Event<boolean>;
handleVisibilityChanged(nowVisible: boolean): void;
}
export interface Selection extends Range {
direction: 'ltr' | 'rtl';
}
export interface Dimension {
width: number;
height: number;
}
export interface TextEditorSelection {
uri: URI
cursor?: Position
selection?: Range
}
export interface RevealPositionOptions {
vertical: 'auto' | 'center' | 'centerIfOutsideViewport';
horizontal?: boolean;
}
export interface RevealRangeOptions {
at: 'auto' | 'center' | 'top' | 'centerIfOutsideViewport';
}
export interface DeltaDecorationParams {
oldDecorations: string[];
newDecorations: EditorDecoration[];
}
export interface ReplaceTextParams {
/**
* the source to edit
*/
source: string;
/**
* the replace operations
*/
replaceOperations: ReplaceOperation[];
}
export interface ReplaceOperation {
/**
* the position that shall be replaced
*/
range: Range;
/**
* the text to replace with
*/
text: string;
}
export namespace TextEditorSelection {
export function is(arg: unknown): arg is TextEditorSelection {
return isObject<TextEditorSelection>(arg) && arg.uri instanceof URI;
}
}
export namespace CustomEditorWidget {
export function is(arg: Widget | undefined): arg is CustomEditorWidget {
return !!arg && 'modelRef' in arg;
}
}
export interface CustomEditorWidget extends Widget {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
readonly modelRef: Reference<any>;
}

View File

@@ -0,0 +1,27 @@
// *****************************************************************************
// Copyright (C) 2018 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
export * from './diff-navigator';
export * from './editor';
export * from './editor-widget';
export * from './editor-manager';
export * from './editor-command';
export * from './editor-menu';
export * from './editor-frontend-module';
export * from './decorations';
export * from './editor-linenumber-contribution';
export * from './split-editor-contribution';
export * from './text-editor-split-contribution';

View File

@@ -0,0 +1,271 @@
// *****************************************************************************
// Copyright (C) 2022 Ericsson and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { injectable, inject } from '@theia/core/shared/inversify';
import { codicon, StatusBar, StatusBarAlignment, StatusBarEntry } from '@theia/core/lib/browser';
import { LanguageService } from '@theia/core/lib/browser/language-service';
import { CommandRegistry, nls } from '@theia/core';
import { TextEditor } from '../editor';
import { EditorCommands } from '../editor-command';
import { LanguageSelector, score } from '../../common/language-selector';
import { AccessibilityInformation } from '@theia/core/lib/common/accessibility';
import URI from '@theia/core/lib/common/uri';
import { CurrentEditorAccess } from '../editor-manager';
import { Severity } from '@theia/core/lib/common/severity';
import { LabelParser } from '@theia/core/lib/browser/label-parser';
/**
* Represents the severity of a language status item.
*/
export enum LanguageStatusSeverity {
Information = 0,
Warning = 1,
Error = 2
}
/**
* Command represents a particular invocation of a registered command.
*/
export interface Command {
/**
* The identifier of the actual command handler.
*/
id: string;
/**
* Title of the command invocation, like "Add local variable 'foo'".
*/
title?: string;
/**
* A tooltip for for command, when represented in the UI.
*/
tooltip?: string;
/**
* Arguments that the command handler should be
* invoked with.
*/
arguments?: unknown[];
}
/**
* A language status item is the preferred way to present language status reports for the active text editors,
* such as selected linter or notifying about a configuration problem.
*/
export interface LanguageStatus {
readonly id: string;
readonly name: string;
readonly selector: LanguageSelector;
readonly severity: Severity;
readonly label: string;
readonly detail: string;
readonly busy: boolean;
readonly source: string;
readonly command: Command | undefined;
readonly accessibilityInfo: AccessibilityInformation | undefined;
}
@injectable()
export class EditorLanguageStatusService {
@inject(StatusBar) protected readonly statusBar: StatusBar;
@inject(LanguageService) protected readonly languages: LanguageService;
@inject(CurrentEditorAccess) protected readonly editorAccess: CurrentEditorAccess;
@inject(CommandRegistry) protected readonly commandRegistry: CommandRegistry;
@inject(LabelParser) protected readonly labelParser: LabelParser;
protected static LANGUAGE_MODE_ID = 'editor-status-language';
protected static LANGUAGE_STATUS_ID = 'editor-language-status-items';
protected readonly status = new Map<number, LanguageStatus>();
protected pinnedCommands = new Set<string>();
setLanguageStatusItem(handle: number, item: LanguageStatus): void {
this.status.set(handle, item);
this.updateLanguageStatusItems();
}
removeLanguageStatusItem(handle: number): void {
this.status.delete(handle);
this.updateLanguageStatusItems();
}
updateLanguageStatus(editor: TextEditor | undefined): void {
if (!editor) {
this.statusBar.removeElement(EditorLanguageStatusService.LANGUAGE_MODE_ID);
return;
}
const language = this.languages.getLanguage(editor.document.languageId);
const languageName = language ? language.name : '';
this.statusBar.setElement(EditorLanguageStatusService.LANGUAGE_MODE_ID, {
text: languageName,
alignment: StatusBarAlignment.RIGHT,
priority: 1,
command: EditorCommands.CHANGE_LANGUAGE.id,
tooltip: nls.localizeByDefault('Select Language Mode')
});
this.updateLanguageStatusItems(editor);
}
protected updateLanguageStatusItems(editor = this.editorAccess.editor): void {
if (!editor) {
this.statusBar.removeElement(EditorLanguageStatusService.LANGUAGE_STATUS_ID);
this.updatePinnedItems();
return;
}
const uri = new URI(editor.document.uri);
const items = Array.from(this.status.values())
.filter(item => score(item.selector, uri.scheme, uri.path.toString(), editor.document.languageId, true))
.sort((left, right) => right.severity - left.severity);
if (!items.length) {
this.statusBar.removeElement(EditorLanguageStatusService.LANGUAGE_STATUS_ID);
return;
}
const severityText = items[0].severity === Severity.Info
? '$(bracket)'
: items[0].severity === Severity.Warning
? '$(bracket-dot)'
: '$(bracket-error)';
this.statusBar.setElement(EditorLanguageStatusService.LANGUAGE_STATUS_ID, {
text: severityText,
alignment: StatusBarAlignment.RIGHT,
priority: 2,
tooltip: this.createTooltip(items),
affinity: { id: EditorLanguageStatusService.LANGUAGE_MODE_ID, alignment: StatusBarAlignment.LEFT, compact: true },
});
this.updatePinnedItems(items);
}
protected updatePinnedItems(items?: LanguageStatus[]): void {
const toRemoveFromStatusBar = new Set(this.pinnedCommands);
items?.forEach(item => {
if (toRemoveFromStatusBar.has(item.id)) {
toRemoveFromStatusBar.delete(item.id);
this.statusBar.setElement(item.id, this.toPinnedItem(item));
}
});
toRemoveFromStatusBar.forEach(id => this.statusBar.removeElement(id));
}
protected toPinnedItem(item: LanguageStatus): StatusBarEntry {
return {
text: item.label,
affinity: { id: EditorLanguageStatusService.LANGUAGE_MODE_ID, alignment: StatusBarAlignment.RIGHT, compact: false },
alignment: StatusBarAlignment.RIGHT,
onclick: item.command && (e => { e.preventDefault(); this.commandRegistry.executeCommand(item.command!.id, ...(item.command?.arguments ?? [])); }),
};
}
protected createTooltip(items: LanguageStatus[]): HTMLElement {
const hoverContainer = document.createElement('div');
hoverContainer.classList.add('hover-row');
for (const item of items) {
const itemContainer = document.createElement('div');
itemContainer.classList.add('hover-language-status');
{
const severityContainer = document.createElement('div');
severityContainer.classList.add('severity', `sev${item.severity}`);
severityContainer.classList.toggle('show', item.severity === Severity.Error || item.severity === Severity.Warning);
{
const severityIcon = document.createElement('span');
severityIcon.className = this.getSeverityIconClasses(item.severity);
severityContainer.appendChild(severityIcon);
}
itemContainer.appendChild(severityContainer);
}
const textContainer = document.createElement('div');
textContainer.className = 'element';
const labelContainer = document.createElement('div');
labelContainer.className = 'left';
const label = document.createElement('span');
label.classList.add('label');
this.renderWithIcons(label, item.busy ? `$(sync~spin)\u00A0\u00A0${item.label}` : item.label);
labelContainer.appendChild(label);
const detail = document.createElement('span');
detail.classList.add('detail');
this.renderWithIcons(detail, item.detail);
labelContainer.appendChild(detail);
textContainer.appendChild(labelContainer);
const commandContainer = document.createElement('div');
commandContainer.classList.add('right');
if (item.command) {
const link = document.createElement('a');
link.classList.add('language-status-link');
link.href = new URI()
.withScheme('command')
.withPath(item.command.id)
.withQuery(item.command.arguments ? encodeURIComponent(JSON.stringify(item.command.arguments)) : '')
.toString(false);
link.onclick = e => { e.preventDefault(); this.commandRegistry.executeCommand(item.command!.id, ...(item.command?.arguments ?? [])); };
link.textContent = item.command.title ?? item.command.id;
link.title = item.command.tooltip ?? '';
link.ariaRoleDescription = 'button';
link.ariaDisabled = 'false';
commandContainer.appendChild(link);
const pinContainer = document.createElement('div');
pinContainer.classList.add('language-status-action-bar');
const pin = document.createElement('a');
this.setPinProperties(pin, item.id);
pin.onclick = e => { e.preventDefault(); this.togglePinned(item); this.setPinProperties(pin, item.id); };
pinContainer.appendChild(pin);
commandContainer.appendChild(pinContainer);
}
textContainer.appendChild(commandContainer);
itemContainer.append(textContainer);
hoverContainer.appendChild(itemContainer);
}
return hoverContainer;
}
protected setPinProperties(pin: HTMLElement, id: string): void {
pin.className = this.pinnedCommands.has(id) ? codicon('pinned', true) : codicon('pin', true);
pin.ariaRoleDescription = 'button';
const pinText = this.pinnedCommands.has(id)
? nls.localizeByDefault('Remove from Status Bar')
: nls.localizeByDefault('Add to Status Bar');
pin.ariaLabel = pinText;
pin.title = pinText;
}
protected togglePinned(item: LanguageStatus): void {
if (this.pinnedCommands.has(item.id)) {
this.pinnedCommands.delete(item.id);
this.statusBar.removeElement(item.id);
} else {
this.pinnedCommands.add(item.id);
this.statusBar.setElement(item.id, this.toPinnedItem(item));
}
}
protected getSeverityIconClasses(severity: Severity): string {
switch (severity) {
case Severity.Error: return codicon('error');
case Severity.Warning: return codicon('info');
default: return codicon('check');
}
}
protected renderWithIcons(host: HTMLElement, text?: string): void {
if (text) {
for (const chunk of this.labelParser.parse(text)) {
if (typeof chunk === 'string') {
host.append(chunk);
} else {
const iconSpan = document.createElement('span');
const className = codicon(chunk.name) + (chunk.animation ? ` fa-${chunk.animation}` : '');
iconSpan.className = className;
host.append(iconSpan);
}
}
}
}
}

View File

@@ -0,0 +1,101 @@
/********************************************************************************
* Copyright (C) 2022 Ericsson and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the Eclipse
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
* with the GNU Classpath Exception which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
********************************************************************************/
/* Copied from https://github.com/microsoft/vscode/blob/7d9b1c37f8e5ae3772782ba3b09d827eb3fdd833/src/vs/workbench/contrib/languageStatus/browser/media/languageStatus.css */
.hover-language-status {
display: flex;
padding: 4px 8px;
}
.hover-language-status:not(:last-child) {
border-bottom: 1px solid var(--theia-notifications-border);
}
.hover-language-status > .severity {
padding-right: 8px;
flex: 1;
margin: auto;
display: none;
}
.hover-language-status > .severity.sev3 {
color: var(--theia-notificationsErrorIcon-foreground);
}
.hover-language-status > .severity.sev2 {
color: var(--theia-notificationsInfoIcon-foreground);
}
.hover-language-status > .severity.show {
display: inherit;
}
.hover-language-status > .element {
display: flex;
justify-content: space-between;
vertical-align: middle;
flex-grow: 100;
}
.hover-language-status > .element > .left > .detail:not(:empty)::before {
/* en-dash */
content: "";
padding: 0 4px;
opacity: 0.6;
}
.hover-language-status > .element > .left > .label:empty {
display: none;
}
.hover-language-status > .element .left {
margin: auto 0;
}
.hover-language-status > .element .right {
margin: auto 0;
display: flex;
}
.hover-language-status > .element .right:not(:empty) {
padding-left: 16px;
}
.hover-language-status > .element .right .language-status-link {
margin: auto 0;
white-space: nowrap;
/* ADDED STYLES - NOT FROM VSCODE */
text-decoration: none;
}
/* ADDED STYLES - NOT FROM VSCODE */
.hover-language-status > .element .right .language-status-link:hover {
color: var(--theia-textLink-foreground);
}
.hover-language-status
> .element
.right
.language-status-action-bar:not(:first-child) {
padding-left: 8px;
}
/* ADDED STYLES - NOT FROM VSCODE */
.hover-language-status > .element .right .language-status-action-bar a {
color: var(--theia-editorHoverWidget-foreground);
}

View File

@@ -0,0 +1,245 @@
// *****************************************************************************
// Copyright (C) 2018 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { enableJSDOM } from '@theia/core/lib/browser/test/jsdom';
let disableJSDOM = enableJSDOM();
import { expect } from 'chai';
import { Container } from '@theia/core/shared/inversify';
import URI from '@theia/core/lib/common/uri';
import { ILogger } from '@theia/core/lib/common/logger';
import { MockLogger } from '@theia/core/lib/common/test/mock-logger';
import { OpenerService } from '@theia/core/lib/browser/opener-service';
import { MockOpenerService } from '@theia/core/lib/browser/test/mock-opener-service';
import { NavigationLocationUpdater } from './navigation-location-updater';
import { NoopNavigationLocationUpdater } from './test/mock-navigation-location-updater';
import { NavigationLocationSimilarity } from './navigation-location-similarity';
import { CursorLocation, Position, NavigationLocation, RecentlyClosedEditor } from './navigation-location';
import { NavigationLocationService } from './navigation-location-service';
disableJSDOM();
describe('navigation-location-service', () => {
let stack: NavigationLocationService;
before(() => {
disableJSDOM = enableJSDOM();
});
after(() => {
disableJSDOM();
});
beforeEach(() => {
stack = init();
});
it('should not allow navigating back when the stack is empty', () => {
expect(stack.canGoBack()).to.be.false;
});
it('should not allow navigating back when the stack has a single location', () => {
stack.register(createCursorLocation());
expect(stack.canGoBack()).to.be.false;
});
it('should allow navigating back when the stack has more than one locations', () => {
stack.register(
createCursorLocation(),
createCursorLocation({ line: 100, character: 100 })
);
expect(stack.canGoBack()).to.be.true;
});
it('should not allow navigating forward when the stack is empty', () => {
expect(stack.canGoForward()).to.be.false;
});
it('should not allow navigating forward when the pointer points to the end last element of the stack', () => {
stack.register(
createCursorLocation(),
createCursorLocation({ line: 100, character: 100 })
);
expect(stack.canGoForward()).to.be.false;
});
it('should not exceed the max stack item', () => {
const max = NavigationLocationService['MAX_STACK_ITEMS'];
const locations: NavigationLocation[] = [...Array(max + 10).keys()].map(i => createCursorLocation({ line: i * 10, character: i }, `file://${i}`));
stack.register(...locations);
expect(stack.locations().length).to.not.be.greaterThan(max);
});
it('should successfully clear the history', () => {
expect(stack['recentlyClosedEditors'].length).equal(0);
const editor = createMockClosedEditor(new URI('file://foo/a.ts'));
stack.addClosedEditor(editor);
expect(stack['recentlyClosedEditors'].length).equal(1);
expect(stack['stack'].length).equal(0);
stack.register(
createCursorLocation(),
);
expect(stack['stack'].length).equal(1);
stack['clearHistory']();
expect(stack['recentlyClosedEditors'].length).equal(0);
expect(stack['stack'].length).equal(0);
});
describe('last-edit-location', async () => {
it('should return with undefined if the stack contains no modifications', () => {
stack.register(
createCursorLocation(),
createCursorLocation({ line: 100, character: 100 })
);
expect(stack.lastEditLocation()).to.be.undefined;
});
it('should return with the location of the last modification', () => {
const expected = NavigationLocation.create('file://path/to/file', {
text: '',
range: { start: { line: 200, character: 0 }, end: { line: 500, character: 0 } },
rangeLength: 0
});
stack.register(
createCursorLocation(),
expected,
createCursorLocation({ line: 100, character: 100 })
);
expect(stack.lastEditLocation()).to.be.deep.equal(expected);
});
it('should return with the location of the last modification even if the pointer is not on the head', async () => {
const modificationLocation = NavigationLocation.create('file://path/to/file', {
text: '',
range: { start: { line: 300, character: 0 }, end: { line: 500, character: 0 } },
rangeLength: 0
});
const expected = NavigationLocation.create('file://path/to/file', {
text: '',
range: { start: { line: 700, character: 0 }, end: { line: 800, character: 0 } },
rangeLength: 0
});
stack.register(
createCursorLocation(),
modificationLocation,
createCursorLocation({ line: 100, character: 100 }),
expected
);
await stack.back();
await stack.back();
expect(stack.currentLocation()).to.be.deep.equal(modificationLocation);
expect(stack.lastEditLocation()).to.be.deep.equal(expected);
});
});
describe('recently-closed-editors', () => {
describe('#getLastClosedEditor', () => {
it('should return the last closed editor from the history', () => {
const uri = new URI('file://foo/a.ts');
stack.addClosedEditor(createMockClosedEditor(uri));
const editor = stack.getLastClosedEditor();
expect(editor?.uri).equal(uri);
});
it('should return `undefined` when no history is found', () => {
expect(stack['recentlyClosedEditors'].length).equal(0);
const editor = stack.getLastClosedEditor();
expect(editor).equal(undefined);
});
it('should not exceed the max history', () => {
expect(stack['recentlyClosedEditors'].length).equal(0);
const max = NavigationLocationService['MAX_RECENTLY_CLOSED_EDITORS'];
for (let i = 0; i < max + 10; i++) {
const uri = new URI(`file://foo/bar-${i}.ts`);
stack.addClosedEditor(createMockClosedEditor(uri));
}
expect(stack['recentlyClosedEditors'].length <= max).be.true;
});
});
describe('#addToRecentlyClosedEditors', () => {
it('should include unique recently closed editors in the history', () => {
expect(stack['recentlyClosedEditors'].length).equal(0);
const a = createMockClosedEditor(new URI('file://foo/a.ts'));
const b = createMockClosedEditor(new URI('file://foo/b.ts'));
stack.addClosedEditor(a);
stack.addClosedEditor(b);
expect(stack['recentlyClosedEditors'].length).equal(2);
});
it('should not include duplicate recently closed editors in the history', () => {
const uri = new URI('file://foo/a.ts');
[1, 2, 3].forEach(i => {
stack.addClosedEditor(createMockClosedEditor(uri));
});
expect(stack['recentlyClosedEditors'].length).equal(1);
});
});
describe('#removeFromRecentlyClosedEditors', () => {
it('should successfully remove editors from the history that match the given editor uri', () => {
expect(stack['recentlyClosedEditors'].length).equal(0);
const editor = createMockClosedEditor(new URI('file://foo/a.ts'));
[1, 2, 3].forEach(() => {
stack['recentlyClosedEditors'].push(editor);
});
expect(stack['recentlyClosedEditors'].length).equal(3);
// Remove the given editor from the recently closed history.
stack['removeClosedEditor'](editor.uri);
expect(stack['recentlyClosedEditors'].length).equal(0);
});
});
});
function createCursorLocation(context: Position = { line: 0, character: 0, }, uri: string = 'file://path/to/file'): CursorLocation {
return NavigationLocation.create(uri, context);
}
function init(): NavigationLocationService {
const container = new Container({ defaultScope: 'Singleton' });
container.bind(NavigationLocationService).toSelf();
container.bind(NavigationLocationSimilarity).toSelf();
container.bind(MockOpenerService).toSelf();
container.bind(MockLogger).toSelf();
container.bind(ILogger).toService(MockLogger);
container.bind(NoopNavigationLocationUpdater).toSelf();
container.bind(NavigationLocationUpdater).toService(NoopNavigationLocationUpdater);
container.bind(OpenerService).toService(MockOpenerService);
return container.get(NavigationLocationService);
}
function createMockClosedEditor(uri: URI): RecentlyClosedEditor {
return { uri, viewState: {} };
}
});

View File

@@ -0,0 +1,368 @@
// *****************************************************************************
// Copyright (C) 2018 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { inject, injectable, named } from '@theia/core/shared/inversify';
import { ILogger } from '@theia/core/lib/common/logger';
import { OpenerService, OpenerOptions, open } from '@theia/core/lib/browser/opener-service';
import { EditorOpenerOptions } from '../editor-manager';
import { NavigationLocationUpdater } from './navigation-location-updater';
import { NavigationLocationSimilarity } from './navigation-location-similarity';
import { NavigationLocation, Range, ContentChangeLocation, RecentlyClosedEditor } from './navigation-location';
import URI from '@theia/core/lib/common/uri';
/**
* The navigation location service.
* It also stores and manages navigation locations and recently closed editors.
*
* Since we update the navigation locations as a side effect of UI events (seting the selection, etc.) We sometimes
* record intermediate locations which are not considered the "final" location we're navigating to.
* In order to remedy, clients should always invoke "startNavigation" before registering locations and
* invoke "endNavigation" when done. Only when no more nested navigations are active ist the last registered
* location transfered to the navigation "stack".
*
*/
@injectable()
export class NavigationLocationService {
private static MAX_STACK_ITEMS = 30;
private static readonly MAX_RECENTLY_CLOSED_EDITORS = 20;
@inject(ILogger) @named('NavigationLocationService')
protected readonly logger: ILogger;
@inject(OpenerService)
protected readonly openerService: OpenerService;
@inject(NavigationLocationUpdater)
protected readonly updater: NavigationLocationUpdater;
@inject(NavigationLocationSimilarity)
protected readonly similarity: NavigationLocationSimilarity;
protected pointer = -1;
protected stack: NavigationLocation[] = [];
protected canRegister = true;
protected _lastEditLocation: ContentChangeLocation | undefined;
protected recentlyClosedEditors: RecentlyClosedEditor[] = [];
protected activeNavigations: { count: number, lastLocation: NavigationLocation[] | undefined } = { count: 0, lastLocation: undefined };
/**
* Start a logical navigation operation. Invoke this before invoking `registerLocations`
*/
startNavigation(): void {
if (this.canRegister) {
this.activeNavigations.count++;
this.logger.debug(`start navigation ${this.activeNavigations.count}`);
this.logger.trace(new Error('start'));
}
}
/**
* End a logical navigation operation. Invoke this before after `registerLocations`
*/
endNavigation(): void {
if (this.canRegister) {
this.activeNavigations.count--;
this.logger.debug(`end navigation ${this.activeNavigations.count}`);
this.logger.trace(new Error('end'));
if (this.activeNavigations.count === 0 && this.activeNavigations.lastLocation !== undefined) {
this.logger.debug(`ending navigation with location ${JSON.stringify(NavigationLocation.toObject(this.activeNavigations.lastLocation[0]))}`);
const locations = this.activeNavigations.lastLocation;
this.activeNavigations.lastLocation = undefined;
this.doRegister(locations);
}
}
}
/**
* Convenience method for executing a navigation operation with proper start and end navigation
* @param navigation The operation changing the location
* @returns the result of the navigation function
*/
navigate<T>(navigation: (navigationServier: NavigationLocationService) => T): T {
this.startNavigation();
try {
return navigation(this);
} finally {
this.endNavigation();
}
}
/**
* Registers the give locations into the service.
*/
register(...locations: NavigationLocation[]): void {
if (this.canRegister) {
if (this.activeNavigations.count > 0) {
this.activeNavigations.lastLocation = locations;
} else {
this.logger.warn('no activative navigation', new Error('No active navigation'));
this.doRegister(locations);
}
}
}
protected doRegister(locations: NavigationLocation[]): void {
const max = this.maxStackItems();
[...locations].forEach(location => {
if (ContentChangeLocation.is(location)) {
this._lastEditLocation = location;
}
const current = this.currentLocation();
this.debug(`Registering new location: ${NavigationLocation.toString(location)}.`);
if (!this.isSimilar(current, location)) {
this.debug('Before location registration.');
this.debug(this.stackDump);
// Just like in VSCode; if we are not at the end of stack, we remove anything after.
if (this.stack.length > this.pointer + 1) {
this.debug(`Discarding all locations after ${this.pointer}.`);
this.stack = this.stack.slice(0, this.pointer + 1);
}
this.stack.push(location);
this.pointer = this.stack.length - 1;
if (this.stack.length > max) {
this.debug('Trimming exceeding locations.');
this.stack.shift();
this.pointer--;
}
this.debug('Updating preceding navigation locations.');
for (let i = this.stack.length - 1; i >= 0; i--) {
const candidate = this.stack[i];
const update = this.updater.affects(candidate, location);
if (update === undefined) {
this.debug(`Erasing obsolete location: ${NavigationLocation.toString(candidate)}.`);
this.stack.splice(i, 1);
this.pointer--;
} else if (typeof update !== 'boolean') {
this.debug(`Updating location at index: ${i} => ${NavigationLocation.toString(candidate)}.`);
this.stack[i] = update;
}
}
this.debug('After location registration.');
this.debug(this.stackDump);
} else {
if (current) {
this.debug(`The new location ${NavigationLocation.toString(location)} is similar to the current one: ${NavigationLocation.toString(current)}. Aborting.`);
}
}
});
}
/**
* Navigates one back. Returns with the previous location, or `undefined` if it could not navigate back.
*/
async back(): Promise<NavigationLocation | undefined> {
this.debug('Navigating back.');
if (this.canGoBack()) {
this.pointer--;
await this.reveal();
this.debug(this.stackDump);
return this.currentLocation();
}
this.debug('Cannot navigate back.');
return undefined;
}
/**
* Navigates one forward. Returns with the next location, or `undefined` if it could not go forward.
*/
async forward(): Promise<NavigationLocation | undefined> {
this.debug('Navigating forward.');
if (this.canGoForward()) {
this.pointer++;
await this.reveal();
this.debug(this.stackDump);
return this.currentLocation();
}
this.debug('Cannot navigate forward.');
return undefined;
}
/**
* Checks whether the service can go [`back`](#back).
*/
canGoBack(): boolean {
return this.pointer >= 1;
}
/**
* Checks whether the service can go [`forward`](#forward).
*/
canGoForward(): boolean {
return this.pointer >= 0 && this.pointer !== this.stack.length - 1;
}
/**
* Returns with all known navigation locations in chronological order.
*/
locations(): ReadonlyArray<NavigationLocation> {
return this.stack;
}
/**
* Returns with the current location.
*/
currentLocation(): NavigationLocation | undefined {
return this.stack[this.pointer];
}
/**
* Returns with the location of the most recent edition if any. If there were no modifications,
* returns `undefined`.
*/
lastEditLocation(): NavigationLocation | undefined {
return this._lastEditLocation;
}
/**
* Clears the total history.
*/
clearHistory(): void {
this.stack = [];
this.pointer = -1;
this._lastEditLocation = undefined;
this.recentlyClosedEditors = [];
}
/**
* Reveals the location argument. If not given, reveals the `current location`. Does nothing, if the argument is `undefined`.
*/
async reveal(location: NavigationLocation | undefined = this.currentLocation()): Promise<void> {
if (location === undefined) {
return;
}
try {
this.canRegister = false;
const { uri } = location;
const options = this.toOpenerOptions(location);
await open(this.openerService, uri, options);
} catch (e) {
this.logger.error(`Error occurred while revealing location: ${NavigationLocation.toString(location)}.`, e);
} finally {
this.canRegister = true;
}
}
/**
* `true` if the two locations are similar.
*/
protected isSimilar(left: NavigationLocation | undefined, right: NavigationLocation | undefined): boolean {
return this.similarity.similar(left, right);
}
/**
* Returns with the number of navigation locations that the application can handle and manage.
* When the number of locations exceeds this number, old locations will be erased.
*/
protected maxStackItems(): number {
return NavigationLocationService.MAX_STACK_ITEMS;
}
/**
* Returns with the opener option for the location argument.
*/
protected toOpenerOptions(location: NavigationLocation): OpenerOptions {
let { start } = NavigationLocation.range(location);
// Here, the `start` and represents the previous state that has been updated with the `text`.
// So we calculate the range by appending the `text` length to the `start`.
if (ContentChangeLocation.is(location)) {
start = { ...start, character: start.character + location.context.text.length };
}
return {
selection: Range.create(start, start)
} as EditorOpenerOptions;
}
private async debug(message: string | (() => string)): Promise<void> {
this.logger.trace(typeof message === 'string' ? message : message());
}
private get stackDump(): string {
return `----- Navigation location stack [${new Date()}] -----
Pointer: ${this.pointer}
${this.stack.map((location, i) => `${i}: ${JSON.stringify(NavigationLocation.toObject(location))}`).join('\n')}
----- o -----`;
}
/**
* Get the recently closed editors stack in chronological order.
*
* @returns readonly closed editors stack.
*/
get closedEditorsStack(): ReadonlyArray<RecentlyClosedEditor> {
return this.recentlyClosedEditors;
}
/**
* Get the last recently closed editor.
*
* @returns the recently closed editor if it exists.
*/
getLastClosedEditor(): RecentlyClosedEditor | undefined {
return this.recentlyClosedEditors[this.recentlyClosedEditors.length - 1];
}
/**
* Add the recently closed editor to the history.
*
* @param editor the recently closed editor.
*/
addClosedEditor(editor: RecentlyClosedEditor): void {
this.removeClosedEditor(editor.uri);
this.recentlyClosedEditors.push(editor);
// Removes the oldest entry from the history if the maximum size is reached.
if (this.recentlyClosedEditors.length > NavigationLocationService.MAX_RECENTLY_CLOSED_EDITORS) {
this.recentlyClosedEditors.shift();
}
}
/**
* Remove all occurrences of the given editor in the history if they exist.
*
* @param uri the uri of the editor that should be removed from the history.
*/
removeClosedEditor(uri: URI): void {
this.recentlyClosedEditors = this.recentlyClosedEditors.filter(e => !uri.isEqual(e.uri));
}
storeState(): object {
const result = {
locations: this.stack.map(NavigationLocation.toObject)
};
return result;
}
restoreState(rawState: object): void {
const state = rawState as {
locations: object[]
};
this.stack = [];
for (let i = 0; i < state.locations.length; i++) {
const location = NavigationLocation.fromObject(state.locations[i]);
if (location) {
this.stack.push(location);
} else {
this.logger.warn(`Could not restore the state of the editor navigation history for ${JSON.stringify(state.locations[i])}`);
}
}
this.pointer = this.stack.length - 1;
}
}

View File

@@ -0,0 +1,46 @@
// *****************************************************************************
// Copyright (C) 2018 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { expect } from 'chai';
import { NavigationLocation } from './navigation-location';
import { NavigationLocationSimilarity } from './navigation-location-similarity';
describe('navigation-location-similarity', () => {
const similarity = new NavigationLocationSimilarity();
it('should never be similar if they belong to different resources', () => {
expect(similarity.similar(
NavigationLocation.create('file:///a', { line: 0, character: 0, }),
NavigationLocation.create('file:///b', { line: 0, character: 0, })
)).to.be.false;
});
it('should be true if the locations are withing the threshold', () => {
expect(similarity.similar(
NavigationLocation.create('file:///a', { start: { line: 0, character: 10 }, end: { line: 0, character: 15 } }),
NavigationLocation.create('file:///a', { start: { line: 10, character: 3 }, end: { line: 0, character: 5 } })
)).to.be.true;
});
it('should be false if the locations are outside of the threshold', () => {
expect(similarity.similar(
NavigationLocation.create('file:///a', { start: { line: 0, character: 10 }, end: { line: 0, character: 15 } }),
NavigationLocation.create('file:///a', { start: { line: 11, character: 3 }, end: { line: 0, character: 5 } })
)).to.be.true;
});
});

View File

@@ -0,0 +1,58 @@
// *****************************************************************************
// Copyright (C) 2018 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { injectable } from '@theia/core/shared/inversify';
import { NavigationLocation } from './navigation-location';
/**
* Service for checking whether two navigation locations are similar or not.
*/
@injectable()
export class NavigationLocationSimilarity {
/**
* The number of lines to move in the editor to justify for new state.
*/
private static EDITOR_SELECTION_THRESHOLD = 10;
/**
* `true` if the `left` and `right` locations are withing +- 10 lines in the same editor. Otherwise, `false`.
*/
similar(left: NavigationLocation | undefined, right: NavigationLocation | undefined): boolean {
if (left === undefined || right === undefined) {
return left === right;
}
if (left.uri.toString() !== right.uri.toString()) {
return false;
}
const leftRange = NavigationLocation.range(left);
const rightRange = NavigationLocation.range(right);
if (leftRange === undefined || rightRange === undefined) {
return leftRange === rightRange;
}
const leftLineNumber = Math.min(leftRange.start.line, leftRange.end.line);
const rightLineNumber = Math.min(rightRange.start.line, rightRange.end.line);
return Math.abs(leftLineNumber - rightLineNumber) < this.getThreshold();
}
protected getThreshold(): number {
return NavigationLocationSimilarity.EDITOR_SELECTION_THRESHOLD;
}
}

View File

@@ -0,0 +1,197 @@
// *****************************************************************************
// Copyright (C) 2018 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { expect } from 'chai';
import { NavigationLocation, Range } from './navigation-location';
import { TextDocumentContentChangeDelta } from '../editor';
import { MockNavigationLocationUpdater } from './test/mock-navigation-location-updater';
describe('navigation-location-updater', () => {
const updater = new MockNavigationLocationUpdater();
describe('contained', () => {
([
[{ start: { line: 4, character: 3 }, end: { line: 4, character: 12 } }, { start: { line: 3, character: 3 }, end: { line: 6, character: 12 } }, true],
[{ start: { line: 4, character: 3 }, end: { line: 5, character: 12 } }, { start: { line: 3, character: 3 }, end: { line: 6, character: 12 } }, true],
[{ start: { line: 3, character: 3 }, end: { line: 3, character: 12 } }, { start: { line: 3, character: 3 }, end: { line: 3, character: 12 } }, true],
[{ start: { line: 3, character: 2 }, end: { line: 3, character: 12 } }, { start: { line: 3, character: 3 }, end: { line: 3, character: 12 } }, false],
[{ start: { line: 2, character: 3 }, end: { line: 3, character: 12 } }, { start: { line: 3, character: 3 }, end: { line: 3, character: 12 } }, false],
[{ start: { line: 3, character: 3 }, end: { line: 3, character: 13 } }, { start: { line: 3, character: 3 }, end: { line: 3, character: 12 } }, false],
[{ start: { line: 3, character: 3 }, end: { line: 4, character: 12 } }, { start: { line: 3, character: 3 }, end: { line: 3, character: 12 } }, false],
] as [Range, Range, boolean][]).forEach(test => {
const [subRange, range, expected] = test;
it(`range ${JSON.stringify(range)} should${!expected ? ' not' : ''} be contained in range ${JSON.stringify(subRange)}`, () => {
expect(updater.contained(subRange, range)).to.be.equal(expected);
});
});
});
describe('affected', () => {
it('should never affect a location if they belong to different resources', () => {
const actual = updater.affects(
NavigationLocation.create('file:///a', { line: 0, character: 0, }),
NavigationLocation.create('file:///b', { line: 0, character: 0, })
);
expect(actual).to.be.false;
});
([
// Spec(1. and 2.)
{
candidate: { start: { line: 3, character: 3 }, end: { line: 3, character: 12 } },
other: { range: { start: { line: 1, character: 6 }, end: { line: 3, character: 2 } }, rangeLength: 78, text: 'x' },
expected: { start: { line: 1, character: 8 }, end: { line: 1, character: 17 } }
},
{
candidate: { start: { line: 3, character: 3 }, end: { line: 4, character: 18 } },
other: { range: { start: { line: 1, character: 6 }, end: { line: 3, character: 2 } }, rangeLength: 78, text: 'x' },
expected: { start: { line: 1, character: 8 }, end: { line: 2, character: 18 } }
},
{
candidate: { start: { line: 3, character: 17 }, end: { line: 3, character: 26 } },
other: { range: { start: { line: 1, character: 3 }, end: { line: 1, character: 12 } }, rangeLength: 9, text: 'x' },
expected: false
},
{
candidate: { start: { line: 3, character: 17 }, end: { line: 3, character: 26 } },
other: { range: { start: { line: 1, character: 3 }, end: { line: 1, character: 12 } }, rangeLength: 9, text: 'a\n\b\nc' },
expected: { start: { line: 5, character: 17 }, end: { line: 5, character: 26 } }
},
// Spec (3.)
{
candidate: { start: { line: 3, character: 17 }, end: { line: 3, character: 26 } },
other: { range: { start: { line: 3, character: 18 }, end: { line: 3, character: 24 } }, rangeLength: 6, text: 'x' },
expected: { start: { line: 3, character: 17 }, end: { line: 3, character: 21 } }
},
{
candidate: { start: { line: 3, character: 17 }, end: { line: 3, character: 26 } },
other: { range: { start: { line: 3, character: 18 }, end: { line: 3, character: 23 } }, rangeLength: 5, text: 'a\n\b\nc' },
expected: { start: { line: 3, character: 17 }, end: { line: 5, character: 4 } }
},
{
candidate: { start: { line: 3, character: 17 }, end: { line: 4, character: 26 } },
other: { range: { start: { line: 3, character: 19 }, end: { line: 4, character: 19 } }, rangeLength: 41, text: 'xxx' },
expected: { start: { line: 3, character: 17 }, end: { line: 3, character: 29 } }
},
{
candidate: { start: { line: 3, character: 17 }, end: { line: 4, character: 26 } },
other: { range: { start: { line: 3, character: 19 }, end: { line: 3, character: 21 } }, rangeLength: 2, text: 'a\nb' },
expected: { start: { line: 3, character: 17 }, end: { line: 5, character: 26 } }
},
{
candidate: { start: { line: 3, character: 17 }, end: { line: 4, character: 26 } },
other: { range: { start: { line: 3, character: 18 }, end: { line: 4, character: 24 } }, rangeLength: 47, text: 'a\nb\nc' },
expected: { start: { line: 3, character: 17 }, end: { line: 5, character: 3 } }
},
// Spec (4.)
{
candidate: { start: { line: 3, character: 17 }, end: { line: 4, character: 26 } },
other: { range: { start: { line: 6, character: 13 }, end: { line: 6, character: 23 } }, rangeLength: 10, text: '' },
expected: false
},
{
candidate: { start: { line: 3, character: 17 }, end: { line: 4, character: 26 } },
other: { range: { start: { line: 6, character: 13 }, end: { line: 6, character: 23 } }, rangeLength: 10, text: 'a\nb\nc' },
expected: false
},
// Spec (5.)
{
candidate: { start: { line: 3, character: 17 }, end: { line: 4, character: 26 } },
other: { range: { start: { line: 3, character: 17 }, end: { line: 4, character: 26 } }, rangeLength: 50, text: '' },
expected: { start: { line: 3, character: 17 }, end: { line: 3, character: 17 } }
},
{
candidate: { start: { line: 3, character: 17 }, end: { line: 4, character: 26 } },
other: { range: { start: { line: 3, character: 17 }, end: { line: 4, character: 27 } }, rangeLength: 51, text: '' },
expected: undefined
},
] as TestInput[]).forEach(test => {
const { other, expected, candidate } = test;
it(`should update the navigation location: ${JSON.stringify(candidate)} => ${JSON.stringify(expected)}`, () => {
if (test.trace) {
trace(test);
}
const actual = updater.affects(
NavigationLocation.create('file:///a', candidate),
NavigationLocation.create('file:///a', other),
);
if (typeof expected === 'boolean') {
expect(actual).to.be.deep.equal(expected);
} else if (expected === undefined) {
expect(actual).to.be.undefined;
} else {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
expect((actual as any).context).to.be.deep.equal(expected);
}
});
});
});
});
interface TestInput {
readonly candidate: Range;
readonly other: TextDocumentContentChangeDelta;
// affected | not affected | should be deleted
readonly expected: Range | false | undefined;
readonly trace?: true;
}
function trace(test: TestInput): void {
const line = 10;
const column = 40;
const doc: string[][] = [];
const printDoc = (text?: string) => {
if (text) {
console.log(`\n START - ${text} `);
}
doc.forEach(row => console.log(row.join('')));
if (text) {
console.log(` END - ${text} \n`);
}
};
const select = (range: Range, p: string) => {
for (let i = range.start.line + 1; i < range.end.line; i++) {
for (let j = 0; j < column; j++) {
doc[i][j] = p;
}
}
if (range.start.line === range.end.line) {
for (let j = range.start.character; j < range.end.character; j++) {
doc[range.start.line][j] = p;
}
} else {
for (let j = range.start.character; j < column; j++) {
doc[range.start.line][j] = p;
}
for (let j = 0; j < range.end.character; j++) {
doc[range.end.line][j] = p;
}
}
};
for (let i = 0; i < line; i++) {
doc.push(Array(column).fill('+'));
}
printDoc('EMPTY DOCUMENT');
select(test.candidate, '_');
printDoc('CANDIDATE SELECTION');
select(test.other.range, 'a');
printDoc('APPLIED MODIFICATION');
// TODO: Show the "doc" after applying the changes.
}

View File

@@ -0,0 +1,220 @@
// *****************************************************************************
// Copyright (C) 2018 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { injectable } from '@theia/core/shared/inversify';
import { NavigationLocation, ContentChangeLocation, CursorLocation, SelectionLocation, Position, Range } from './navigation-location';
/**
* A navigation location updater that is responsible for adapting editor navigation locations.
*
* 1. Inserting or deleting text before the position shifts the position accordingly.
* 2. Inserting text at the position offset shifts the position accordingly.
* 3. Inserting or deleting text strictly contained by the position shrinks or stretches the position.
* 4. Inserting or deleting text after a position does not affect the position.
* 5. Deleting text which strictly contains the position deletes the position.
* Note that the position is not deleted if its only shrunken to length zero. To delete a position, the modification must delete from
* strictly before to strictly after the position.
* 6. Replacing text contained by the position shrinks or expands the position (but does not shift it), such that the final position
* contains the original position and the replacing text.
* 7. Replacing text overlapping the position in other ways is considered as a sequence of first deleting the replaced text and
* afterwards inserting the new text. Thus, a position is shrunken and can then be shifted (if the replaced text overlaps the offset of the position).
*/
@injectable()
export class NavigationLocationUpdater {
/**
* Checks whether `candidateLocation` has to be updated when applying `other`.
* - `false` if the `other` does not affect the `candidateLocation`.
* - A `NavigationLocation` object if the `candidateLocation` has to be replaced with the return value.
* - `undefined` if the candidate has to be deleted.
*
* If the `otherLocation` is not a `ContentChangeLocation` or it does not contain any actual content changes, this method returns with `false`
*/
affects(candidateLocation: NavigationLocation, otherLocation: NavigationLocation): false | NavigationLocation | undefined {
if (!ContentChangeLocation.is(otherLocation)) {
return false;
}
if (candidateLocation.uri.toString() !== otherLocation.uri.toString()) {
return false;
}
const candidate = NavigationLocation.range(candidateLocation);
const other = NavigationLocation.range(otherLocation);
if (candidate === undefined || other === undefined) {
return false;
}
const { uri, type } = candidateLocation;
const modification = otherLocation.context.text;
const newLineCount = modification.split(/[\n\r]/g).length - 1;
// Spec (1. and 2.)
if (other.end.line < candidate.start.line
|| (other.end.line === candidate.start.line && other.end.character <= candidate.start.character)) {
// Shortcut for the general case. The user is typing above the candidate range. Nothing to do.
if (other.start.line === other.end.line && newLineCount === 0) {
return false;
}
const lineDiff = other.start.line - other.end.line + newLineCount;
let startCharacter = candidate.start.character;
let endCharacter = candidate.end.character;
if (other.start.line !== other.end.line) {
startCharacter = other.start.character + (candidate.start.character - other.end.character) + (modification.length - (modification.lastIndexOf('\n') + 1));
endCharacter = candidate.start.line === candidate.end.line
? candidate.end.character + startCharacter - candidate.start.character
: candidate.end.character;
}
const context = this.handleBefore(candidateLocation, other, lineDiff, startCharacter, endCharacter);
return {
uri,
type,
context
};
}
// Spec (3., 5., and 6.)
if (this.contained(other, candidate)) {
const endLine = candidate.end.line - other.end.line + candidate.start.line + newLineCount;
let endCharacter = candidate.end.character - (other.end.character - other.start.character) + modification.length;
if (newLineCount > 0) {
if (candidate.end.line === other.end.line) {
endCharacter = modification.length - (modification.lastIndexOf('\n') + 1) + (candidate.end.character - other.end.character);
} else {
endCharacter = endCharacter - 1;
}
}
const context = this.handleInside(candidateLocation, endLine, endCharacter);
return {
uri,
type,
context
};
}
// Spec (5.)
if (other.start.line === candidate.start.line && other.start.character === candidate.start.character
&& (other.end.line > candidate.end.line || (other.end.line === candidate.end.line && other.end.character > candidate.end.character))) {
return undefined;
}
// Spec (4.)
if (candidate.end.line < other.start.line
|| (candidate.end.line === other.start.line && candidate.end.character < other.end.character)) {
return false;
}
return false;
}
protected handleInside(candidate: NavigationLocation, endLine: number, endCharacter: number): NavigationLocation.Context {
if (CursorLocation.is(candidate)) {
throw new Error('Modifications are not allowed inside a cursor location.');
}
const { start } = NavigationLocation.range(candidate);
const range = {
start,
end: {
line: endLine,
character: endCharacter
}
};
if (SelectionLocation.is(candidate)) {
return range;
}
if (ContentChangeLocation.is(candidate)) {
const { rangeLength, text } = candidate.context;
return {
range,
rangeLength,
text
};
}
throw new Error(`Unexpected navigation location: ${NavigationLocation.toString(candidate)}.`);
}
protected handleBefore(candidate: NavigationLocation, modification: Range, lineDiff: number, startCharacter: number, endCharacter: number): NavigationLocation.Context {
let range = NavigationLocation.range(candidate);
range = this.shiftLine(range, lineDiff);
range = {
start: {
line: range.start.line,
character: startCharacter
},
end: {
line: range.end.line,
character: endCharacter
}
};
if (CursorLocation.is(candidate)) {
return range.start;
}
if (SelectionLocation.is(candidate)) {
return range;
}
if (ContentChangeLocation.is(candidate)) {
const { rangeLength, text } = candidate.context;
return {
range,
rangeLength,
text
};
}
throw new Error(`Unexpected navigation location: ${NavigationLocation.toString(candidate)}.`);
}
protected shiftLine(position: Position, diff: number): Position;
protected shiftLine(range: Range, diff: number): Range;
protected shiftLine(input: Position | Range, diff: number): Position | Range {
if (Position.is(input)) {
const { line, character } = input;
return {
line: line + diff,
character
};
}
const { start, end } = input;
return {
start: this.shiftLine(start, diff),
end: this.shiftLine(end, diff)
};
}
/**
* `true` if `subRange` is strictly contained in the `range`. Otherwise, `false`.
*/
protected contained(subRange: Range, range: Range): boolean {
if (subRange.start.line > range.start.line && subRange.end.line < range.end.line) {
return true;
}
if (subRange.start.line < range.start.line || subRange.end.line > range.end.line) {
return false;
}
if (subRange.start.line === range.start.line && subRange.start.character < range.start.character) {
return false;
}
if (subRange.end.line === range.end.line && subRange.end.character > range.end.character) {
return false;
}
return true;
}
}

View File

@@ -0,0 +1,418 @@
// *****************************************************************************
// Copyright (C) 2018 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import URI from '@theia/core/lib/common/uri';
import { Position, Range, TextDocumentContentChangeDelta } from '../editor';
export { Position, Range };
export namespace NavigationLocation {
/**
* The navigation location type.
*/
export enum Type {
/**
* Cursor position change type.
*/
CURSOR,
/**
* Text selection change type.
*/
SELECTION,
/**
* Content change type.
*/
CONTENT_CHANGE
}
/**
* The type of the context for the navigation location.
*/
export type Context = Position | Range | TextDocumentContentChangeDelta;
export namespace Context {
/**
* Returns with the type for the context.
*/
export function getType(context: Context): Type {
if (Position.is(context)) {
return Type.CURSOR;
}
if (Range.is(context)) {
return Type.SELECTION;
}
if (TextDocumentContentChangeDelta.is(context)) {
return Type.CONTENT_CHANGE;
}
throw new Error(`Unexpected context for type: ${context}.`);
}
}
}
/**
* Representation of a navigation location in a text editor.
*/
export interface NavigationLocation {
/**
* The URI of the resource opened in the editor.
*/
readonly uri: URI;
/**
* The type of the navigation location.
*/
readonly type: NavigationLocation.Type;
/**
* Context of the navigation location.
*/
readonly context: NavigationLocation.Context;
}
export namespace NavigationLocation {
/**
* Transforms the location into an object that can be safely serialized.
*/
export function toObject(location: NavigationLocation): object {
const { uri, type } = location;
const context = (() => {
if (CursorLocation.is(location)) {
return CursorLocation.toObject(location.context);
}
if (SelectionLocation.is(location)) {
return SelectionLocation.toObject(location.context);
}
if (ContentChangeLocation.is(location)) {
return ContentChangeLocation.toObject(location.context);
}
})();
return {
uri: uri.toString(),
type,
context
};
}
/**
* Returns with the navigation location object from its serialized counterpart.
*/
export function fromObject(object: Partial<NavigationLocation>): NavigationLocation | undefined {
const { uri, type } = object;
if (uri !== undefined && type !== undefined && object.context !== undefined) {
const context = (() => {
switch (type) {
case NavigationLocation.Type.CURSOR: return CursorLocation.fromObject(object.context as Position);
case NavigationLocation.Type.SELECTION: return SelectionLocation.fromObject(object.context as Range);
case NavigationLocation.Type.CONTENT_CHANGE: return ContentChangeLocation.fromObject(object.context as TextDocumentContentChangeDelta);
}
})();
if (context) {
return {
uri: toUri(uri),
context,
type
};
}
}
return undefined;
}
/**
* Returns with the context of the location as a `Range`.
*/
export function range(location: NavigationLocation): Range {
if (CursorLocation.is(location)) {
return Range.create(location.context, location.context);
}
if (SelectionLocation.is(location)) {
return location.context;
}
if (ContentChangeLocation.is(location)) {
return location.context.range;
}
throw new Error(`Unexpected navigation location: ${location}.`);
}
/**
* Creates a new cursor location.
*/
export function create(uri: URI | { uri: URI } | string, context: Position): CursorLocation;
/**
* Creates a new selection location.
*/
export function create(uri: URI | { uri: URI } | string, context: Range): SelectionLocation;
/**
* Creates a new text content change location type.
*/
export function create(uri: URI | { uri: URI } | string, context: TextDocumentContentChangeDelta): ContentChangeLocation;
/**
* Creates a new navigation location object.
*/
export function create(uri: URI | { uri: URI } | string, context: NavigationLocation.Context): NavigationLocation {
const type = NavigationLocation.Context.getType(context);
return {
uri: toUri(uri),
type,
context
};
}
/**
* Returns with the human-consumable (JSON) string representation of the location argument.
*/
export function toString(location: NavigationLocation): string {
return JSON.stringify(toObject(location));
}
}
function toUri(arg: URI | { uri: URI } | string): URI {
if (arg instanceof URI) {
return arg;
}
if (typeof arg === 'string') {
return new URI(arg);
}
return arg.uri;
}
/**
* Representation of a closed editor.
*/
export interface RecentlyClosedEditor {
/**
* The uri of the closed editor.
*/
readonly uri: URI,
/**
* The serializable view state of the closed editor.
*/
readonly viewState: object
}
export namespace RecentlyClosedEditor {
/**
* Transform a RecentlyClosedEditor into an object for storing.
*
* @param closedEditor the editor needs to be transformed.
*/
export function toObject(closedEditor: RecentlyClosedEditor): object {
const { uri, viewState } = closedEditor;
return {
uri: uri.toString(),
viewState: viewState
};
}
/**
* Transform the given object to a RecentlyClosedEditor object if possible.
*/
export function fromObject(object: Partial<RecentlyClosedEditor>): RecentlyClosedEditor | undefined {
const { uri, viewState } = object;
if (uri !== undefined && viewState !== undefined) {
return {
uri: toUri(uri),
viewState: viewState
};
}
return undefined;
}
}
/**
* Navigation location representing the cursor location change.
*/
export interface CursorLocation extends NavigationLocation {
/**
* The type is always `cursor`.
*/
readonly type: NavigationLocation.Type.CURSOR;
/**
* The context for the location, that is always a position.
*/
readonly context: Position;
}
export namespace CursorLocation {
/**
* `true` if the argument is a cursor location. Otherwise, `false`.
*/
export function is(location: NavigationLocation): location is CursorLocation {
return location.type === NavigationLocation.Type.CURSOR;
}
/**
* Returns with the serialized format of the position argument.
*/
export function toObject(context: Position): object {
const { line, character } = context;
return {
line,
character
};
}
/**
* Returns with the position from its serializable counterpart, or `undefined`.
*/
export function fromObject(object: Partial<Position>): Position | undefined {
if (object.line !== undefined && object.character !== undefined) {
const { line, character } = object;
return {
line,
character
};
}
return undefined;
}
}
/**
* Representation of a selection location.
*/
export interface SelectionLocation extends NavigationLocation {
/**
* The `selection` type.
*/
readonly type: NavigationLocation.Type.SELECTION;
/**
* The context of the selection; a range.
*/
readonly context: Range;
}
export namespace SelectionLocation {
/**
* `true` if the argument is a selection location.
*/
export function is(location: NavigationLocation): location is SelectionLocation {
return location.type === NavigationLocation.Type.SELECTION;
}
/**
* Converts the range argument into a serializable object.
*/
export function toObject(context: Range): object {
const { start, end } = context;
return {
start: CursorLocation.toObject(start),
end: CursorLocation.toObject(end)
};
}
/**
* Creates a range object from its serializable counterpart. Returns with `undefined` if the argument cannot be converted into a range.
*/
export function fromObject(object: Partial<Range>): Range | undefined {
if (!!object.start && !!object.end) {
const start = CursorLocation.fromObject(object.start);
const end = CursorLocation.fromObject(object.end);
if (start && end) {
return {
start,
end
};
}
}
return undefined;
}
}
/**
* Content change location type.
*/
export interface ContentChangeLocation extends NavigationLocation {
/**
* The type, that is always `content change`.
*/
readonly type: NavigationLocation.Type.CONTENT_CHANGE;
/**
* A text document content change deltas as the context.
*/
readonly context: TextDocumentContentChangeDelta;
}
export namespace ContentChangeLocation {
/**
* `true` if the argument is a content change location. Otherwise, `false`.
*/
export function is(location: NavigationLocation): location is ContentChangeLocation {
return location.type === NavigationLocation.Type.CONTENT_CHANGE;
}
/**
* Returns with a serializable object representing the arguments.
*/
export function toObject(context: TextDocumentContentChangeDelta): object {
return {
range: SelectionLocation.toObject(context.range),
rangeLength: context.rangeLength,
text: context.text
};
}
/**
* Returns with a text document change delta for the argument. `undefined` if the argument cannot be mapped to a content change delta.
*/
export function fromObject(object: Partial<TextDocumentContentChangeDelta>): TextDocumentContentChangeDelta | undefined {
if (!!object.range && object.rangeLength !== undefined && object.text !== undefined) {
const range = SelectionLocation.fromObject(object.range!);
const rangeLength = object.rangeLength;
const text = object.text;
if (!!range) {
return {
range,
rangeLength: rangeLength!,
text: text!
};
}
} else {
return undefined;
}
}
}

View File

@@ -0,0 +1,41 @@
// *****************************************************************************
// Copyright (C) 2018 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { Range } from '../navigation-location';
import { NavigationLocationUpdater } from '../navigation-location-updater';
/**
* Navigation location updater with increased method visibility for testing.
*/
export class MockNavigationLocationUpdater extends NavigationLocationUpdater {
override contained(subRange: Range, range: Range): boolean {
return super.contained(subRange, range);
}
}
/**
* NOOP navigation location updater for testing. Use this, if you want to avoid any
* location updates during the tests.
*/
export class NoopNavigationLocationUpdater extends NavigationLocationUpdater {
override affects(): false {
return false;
}
}

View File

@@ -0,0 +1,94 @@
// *****************************************************************************
// Copyright (C) 2021 SAP SE or an SAP affiliate company and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { injectable, inject } from '@theia/core/shared/inversify';
import { CancellationToken, nls, QuickPickItemOrSeparator } from '@theia/core/lib/common';
import { LabelProvider } from '@theia/core/lib/browser/label-provider';
import { QuickAccessProvider, QuickAccessRegistry, QuickAccessContribution } from '@theia/core/lib/browser/quick-input/quick-access';
import { filterItems, QuickPickItem, QuickPickSeparator } from '@theia/core/lib/browser/quick-input/quick-input-service';
import { ApplicationShell, NavigatableWidget, TabBar, Widget } from '@theia/core/lib/browser';
@injectable()
export class QuickEditorService implements QuickAccessContribution, QuickAccessProvider {
static PREFIX = 'edt ';
@inject(LabelProvider) protected readonly labelProvider: LabelProvider;
@inject(QuickAccessRegistry) protected readonly quickAccessRegistry: QuickAccessRegistry;
@inject(ApplicationShell) protected readonly shell: ApplicationShell;
protected groupLocalizations: string[] = [];
registerQuickAccessProvider(): void {
this.quickAccessRegistry.registerQuickAccessProvider({
getInstance: () => this,
prefix: QuickEditorService.PREFIX,
placeholder: '',
helpEntries: [{ description: 'Show All Opened Editors', needsEditor: false }]
});
}
getPicks(filter: string, token: CancellationToken): (QuickPickItem | QuickPickSeparator)[] {
const editorItems: QuickPickItemOrSeparator[] = [];
const hasUri = (widget: Widget): widget is NavigatableWidget => Boolean(NavigatableWidget.getUri(widget));
const handleWidgets = (widgets: NavigatableWidget[], label: string) => {
if (widgets.length) {
editorItems.push({ type: 'separator', label });
}
editorItems.push(...widgets.map(widget => this.toItem(widget)));
};
const handleSplittableArea = (tabbars: TabBar<Widget>[], labelPrefix: string) => {
tabbars.forEach((tabbar, index) => {
const editorsOnTabbar = tabbar.titles.reduce<NavigatableWidget[]>((widgets, title) => {
if (hasUri(title.owner)) {
widgets.push(title.owner);
}
return widgets;
}, []);
const label = tabbars.length > 1 ? `${labelPrefix} ${this.getGroupLocalization(index)}` : labelPrefix;
handleWidgets(editorsOnTabbar, label);
});
};
handleSplittableArea(this.shell.mainAreaTabBars, ApplicationShell.areaLabels.main);
handleSplittableArea(this.shell.bottomAreaTabBars, ApplicationShell.areaLabels.bottom);
for (const area of ['left', 'right'] as ApplicationShell.Area[]) {
const editorsInArea = this.shell.getWidgets(area).filter(hasUri);
handleWidgets(editorsInArea, ApplicationShell.areaLabels[area]);
}
return filterItems(editorItems.slice(), filter);
}
protected getGroupLocalization(index: number): string {
return this.groupLocalizations[index] || nls.localizeByDefault('Group {0}', index + 1);
}
protected toItem(widget: NavigatableWidget): QuickPickItem {
const uri = NavigatableWidget.getUri(widget)!;
const icon = this.labelProvider.getIcon(uri);
const iconClasses: string[] = icon ? icon.split(' ').concat('file-icon') : [];
return {
label: this.labelProvider.getName(uri),
description: this.labelProvider.getDetails(uri),
iconClasses,
ariaLabel: uri.path.fsPath(),
alwaysShow: true,
execute: () => this.shell.activateWidget(widget.id),
};
}
}

View File

@@ -0,0 +1,46 @@
// *****************************************************************************
// Copyright (C) 2025 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { Widget, DockLayout } from '@theia/core/lib/browser';
/**
* Symbol used to bind SplitEditorContribution implementations.
*/
export const SplitEditorContribution = Symbol('SplitEditorContribution');
/**
* A contribution interface for handling split operations on different editor types.
* Implementations should handle specific editor widget types (e.g., text editors, notebook editors).
*
* @template W the specific widget type this contribution handles
*/
export interface SplitEditorContribution<W extends Widget = Widget> {
/**
* Determines whether this contribution can handle the split operation for the given widget.
* @param widget the widget to check
* @returns a priority number (higher means higher priority), or 0 if this contribution cannot handle the widget
*/
canHandle(widget: Widget): number;
/**
* Splits the given widget according to the specified split mode.
* @param widget the widget to split
* @param splitMode the direction in which to split
* @returns the newly created widget, or undefined if the split operation failed
*/
split(widget: W, splitMode: DockLayout.InsertMode): Promise<W | undefined>;
}

View File

@@ -0,0 +1,20 @@
/********************************************************************************
* Copyright (C) 2020 TypeFox and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the Eclipse
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
* with the GNU Classpath Exception which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
********************************************************************************/
.theia-editor {
height: 100%;
overflow: hidden;
}

View File

@@ -0,0 +1,50 @@
// *****************************************************************************
// Copyright (C) 2025 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { inject, injectable } from '@theia/core/shared/inversify';
import { Widget, DockLayout } from '@theia/core/lib/browser';
import { SplitEditorContribution } from './split-editor-contribution';
import { EditorWidget } from './editor-widget';
import { EditorManager } from './editor-manager';
/**
* Implementation of SplitEditorContribution for text editors (EditorWidget).
*/
@injectable()
export class TextEditorSplitContribution implements SplitEditorContribution<EditorWidget> {
@inject(EditorManager)
protected readonly editorManager: EditorManager;
canHandle(widget: Widget): number {
return widget instanceof EditorWidget ? 100 : 0;
}
async split(widget: EditorWidget, splitMode: DockLayout.InsertMode): Promise<EditorWidget | undefined> {
const selection = widget.editor.selection;
const newEditor = await this.editorManager.openToSide(widget.editor.uri, {
selection,
widgetOptions: { mode: splitMode, ref: widget }
});
// Preserve the view state (scroll position, etc.) from the original editor
const oldEditorState = widget.editor.storeViewState();
newEditor.editor.restoreViewState(oldEditorState);
return newEditor;
}
}

View File

@@ -0,0 +1,120 @@
// *****************************************************************************
// Copyright (C) 2021 SAP SE or an SAP affiliate company and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// copied and modified from https://github.com/microsoft/vscode/blob/53eac52308c4611000a171cc7bf1214293473c78/src/vs/platform/undoRedo/common/undoRedoService.ts#
import { injectable } from '@theia/core/shared/inversify';
import URI from '@theia/core/lib/common/uri';
@injectable()
export class UndoRedoService {
private readonly editStacks = new Map<string, ResourceEditStack>();
pushElement(resource: URI, undo: () => Promise<void>, redo: () => Promise<void>): void {
let editStack: ResourceEditStack;
if (this.editStacks.has(resource.toString())) {
editStack = this.editStacks.get(resource.toString())!;
} else {
editStack = new ResourceEditStack();
this.editStacks.set(resource.toString(), editStack);
}
editStack.pushElement({ undo, redo });
}
removeElements(resource: URI): void {
if (this.editStacks.has(resource.toString())) {
this.editStacks.delete(resource.toString());
}
}
undo(resource: URI): void {
if (!this.editStacks.has(resource.toString())) {
return;
}
const editStack = this.editStacks.get(resource.toString())!;
const element = editStack.getClosestPastElement();
if (!element) {
return;
}
editStack.moveBackward(element);
element.undo();
}
redo(resource: URI): void {
if (!this.editStacks.has(resource.toString())) {
return;
}
const editStack = this.editStacks.get(resource.toString())!;
const element = editStack.getClosestFutureElement();
if (!element) {
return;
}
editStack.moveForward(element);
element.redo();
}
}
interface StackElement {
undo(): Promise<void> | void;
redo(): Promise<void> | void;
}
export class ResourceEditStack {
private past: StackElement[];
private future: StackElement[];
constructor() {
this.past = [];
this.future = [];
}
pushElement(element: StackElement): void {
this.future = [];
this.past.push(element);
}
getClosestPastElement(): StackElement | undefined {
if (this.past.length === 0) {
return undefined;
}
return this.past[this.past.length - 1];
}
getClosestFutureElement(): StackElement | undefined {
if (this.future.length === 0) {
return undefined;
}
return this.future[this.future.length - 1];
}
moveBackward(element: StackElement): void {
this.past.pop();
this.future.push(element);
}
moveForward(element: StackElement): void {
this.future.pop();
this.past.push(element);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,229 @@
// *****************************************************************************
// Copyright (C) 2018 Ericsson and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { interfaces } from '@theia/core/shared/inversify';
import {
createPreferenceProxy,
PreferenceProxy,
PreferenceService,
PreferenceChangeEvent,
PreferenceScope,
PreferenceContribution,
PreferenceProxyFactory,
PreferenceSchema,
} from '@theia/core/lib/common/preferences';
import { nls } from '@theia/core/lib/common/nls';
import { environment } from '@theia/core';
import { editorGeneratedPreferenceProperties, GeneratedEditorPreferences } from './editor-generated-preference-schema';
/* eslint-disable max-len,no-null/no-null */
// #region src/vs/workbench/contrib/codeActions/browser/codeActionsContribution.ts
const codeActionsContributionSchema: PreferenceSchema['properties'] = {
'editor.codeActionsOnSave': {
oneOf: [
{
type: 'object',
properties: {
'source.fixAll': {
type: 'boolean',
description: nls.localizeByDefault('Controls whether \'{0}\' actions should be run on file save.', 'quickfix')
}
},
additionalProperties: {
type: 'boolean'
},
},
{
type: 'array',
items: { type: 'string' }
}
],
default: {},
markdownDescription: nls.localizeByDefault('Run Code Actions for the editor on save. Code Actions must be specified and the editor must not be shutting down. When {0} is set to `afterDelay`, Code Actions will only be run when the file is saved explicitly. Example: `"source.organizeImports": "explicit" `'),
scope: PreferenceScope.Folder,
overridable: true
}
};
interface CodeActionsContributionProperties {
'editor.codeActionsOnSave': string[] | ({ 'source.fixAll': boolean } & Record<string, boolean>)
}
// #endregion
// #region src/vs/workbench/contrib/files/browser/files.contribution.ts
const fileContributionSchema: PreferenceSchema['properties'] = {
'editor.formatOnSave': {
'type': 'boolean',
'description': nls.localizeByDefault('Format a file on save. A formatter must be available and the editor must not be shutting down. When {0} is set to `afterDelay`, the file will only be formatted when saved explicitly.', '`#files.autoSave#`'),
'scope': PreferenceScope.Folder,
overridable: true
},
'editor.formatOnSaveMode': {
'type': 'string',
'default': 'file',
'enum': [
'file',
'modifications',
'modificationsIfAvailable'
],
'enumDescriptions': [
nls.localizeByDefault('Format the whole file.'),
nls.localizeByDefault("Format modifications. Requires source control and a formatter that supports 'Format Selection'."),
nls.localizeByDefault('Will attempt to format modifications only (requires source control and a formatter that supports \'Format Selection\'). If source control can\'t be used, then the whole file will be formatted.'),
],
'markdownDescription': nls.localizeByDefault('Controls if format on save formats the whole file or only modifications. Only applies when `#editor.formatOnSave#` is enabled.'),
'scope': PreferenceScope.Folder,
overridable: true
},
// Include this, even though it is not strictly an `editor`preference.
'files.eol': {
'type': 'string',
'enum': [
'\n',
'\r\n',
'auto'
],
'enumDescriptions': [
nls.localizeByDefault('LF'),
nls.localizeByDefault('CRLF'),
nls.localizeByDefault('Uses operating system specific end of line character.')
],
'default': 'auto',
'description': nls.localizeByDefault('The default end of line character.'),
scope: PreferenceScope.Folder,
overridable: true
},
// We used to call these `editor.autoSave` and `editor.autoSaveDelay`.
'files.autoSave': {
'type': 'string',
'enum': ['off', 'afterDelay', 'onFocusChange', 'onWindowChange'],
'markdownEnumDescriptions': [
nls.localizeByDefault('An editor with changes is never automatically saved.'),
nls.localizeByDefault('An editor with changes is automatically saved after the configured `#files.autoSaveDelay#`.'),
nls.localizeByDefault('An editor with changes is automatically saved when the editor loses focus.'),
nls.localizeByDefault('An editor with changes is automatically saved when the window loses focus.')
],
'default': environment.electron.is() ? 'off' : 'afterDelay',
'markdownDescription': nls.localizeByDefault('Controls [auto save](https://code.visualstudio.com/docs/editor/codebasics#_save-auto-save) of editors that have unsaved changes.')
},
'files.autoSaveDelay': {
'type': 'number',
'default': 1000,
'minimum': 0,
'markdownDescription': nls.localizeByDefault('Controls the delay in milliseconds after which an editor with unsaved changes is saved automatically. Only applies when `#files.autoSave#` is set to `{0}`.', 'afterDelay')
},
'files.refactoring.autoSave': {
'type': 'boolean',
'default': true,
'description': nls.localizeByDefault('Controls if files that were part of a refactoring are saved automatically')
}
};
interface FileContributionEditorPreferences {
'editor.formatOnSave': boolean;
'editor.formatOnSaveMode': 'file' | 'modifications' | 'modificationsIfAvailable';
'files.eol': '\n' | '\r\n' | 'auto';
'files.autoSave': 'off' | 'afterDelay' | 'onFocusChange' | 'onWindowChange';
'files.autoSaveDelay': number;
'files.refactoring.autoSave': boolean
}
// #endregion
// #region src/vs/workbench/contrib/format/browser/formatActionsMultiple.ts
// This schema depends on a lot of private stuff in the file, so this is a stripped down version.
const formatActionsMultipleSchema: PreferenceSchema['properties'] = {
'editor.defaultFormatter': {
description: nls.localizeByDefault('Defines a default formatter which takes precedence over all other formatter settings. Must be the identifier of an extension contributing a formatter.'),
type: ['string', 'null'],
default: null,
}
};
interface FormatActionsMultipleProperties {
'editor.defaultFormatter': string | null;
}
// #endregion
// #region Custom Theia extensions to editor preferences
const theiaEditorSchema: PreferenceSchema['properties'] = {
'editor.formatOnSaveTimeout': {
'type': 'number',
'default': 750,
'description': nls.localize('theia/editor/formatOnSaveTimeout', 'Timeout in milliseconds after which the formatting that is run on file save is cancelled.')
},
'editor.history.persistClosedEditors': {
'type': 'boolean',
'default': false,
'description': nls.localize('theia/editor/persistClosedEditors', 'Controls whether to persist closed editor history for the workspace across window reloads.')
},
};
interface TheiaEditorProperties {
'editor.formatOnSaveTimeout': number;
'editor.history.persistClosedEditors': boolean;
}
// #endregion
const combinedProperties = {
...editorGeneratedPreferenceProperties,
...codeActionsContributionSchema,
...fileContributionSchema,
...formatActionsMultipleSchema,
...theiaEditorSchema
};
export const editorPreferenceSchema: PreferenceSchema = {
scope: PreferenceScope.Folder,
defaultOverridable: true,
properties: combinedProperties,
};
export interface EditorConfiguration extends GeneratedEditorPreferences,
CodeActionsContributionProperties,
FileContributionEditorPreferences,
FormatActionsMultipleProperties,
TheiaEditorProperties { }
export type EndOfLinePreference = '\n' | '\r\n' | 'auto';
export type EditorPreferenceChange = PreferenceChangeEvent<EditorConfiguration>;
export const EditorPreferenceContribution = Symbol('EditorPreferenceContribution');
export const EditorPreferences = Symbol('EditorPreferences');
export type EditorPreferences = PreferenceProxy<EditorConfiguration>;
/**
* @deprecated @since 1.23.0
*
* By default, editor preferences now use a validated preference proxy created by the PreferenceProxyFactory binding.
* This function will create an unvalidated preference proxy.
* See {@link bindEditorPreferences}
*/
export function createEditorPreferences(preferences: PreferenceService, schema: PreferenceSchema = editorPreferenceSchema): EditorPreferences {
return createPreferenceProxy(preferences, schema);
}
export function bindEditorPreferences(bind: interfaces.Bind): void {
bind(EditorPreferences).toDynamicValue(ctx => {
const factory = ctx.container.get<PreferenceProxyFactory>(PreferenceProxyFactory);
return factory(editorPreferenceSchema);
}).inSingletonScope();
bind(EditorPreferenceContribution).toConstantValue({ schema: editorPreferenceSchema });
bind(PreferenceContribution).toService(EditorPreferenceContribution);
}

View File

@@ -0,0 +1,104 @@
// *****************************************************************************
// Copyright (C) 2020 Red Hat, Inc. and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { match as matchGlobPattern } from '@theia/core/lib/common/glob';
export interface RelativePattern {
base: string;
pattern: string;
pathToRelative(from: string, to: string): string;
}
export interface LanguageFilter {
language?: string;
scheme?: string;
pattern?: string | RelativePattern;
hasAccessToAllModels?: boolean;
}
export type LanguageSelector = string | LanguageFilter | (string | LanguageFilter)[];
export function score(selector: LanguageSelector | undefined, uriScheme: string, path: string, candidateLanguage: string, candidateIsSynchronized: boolean): number {
if (Array.isArray(selector)) {
let ret = 0;
for (const filter of selector) {
const value = score(filter, uriScheme, path, candidateLanguage, candidateIsSynchronized);
if (value === 10) {
return value;
}
if (value > ret) {
ret = value;
}
}
return ret;
} else if (typeof selector === 'string') {
if (!candidateIsSynchronized) {
return 0;
}
if (selector === '*') {
return 5;
} else if (selector === candidateLanguage) {
return 10;
} else {
return 0;
}
} else if (selector) {
const { language, pattern, scheme, hasAccessToAllModels } = selector;
if (!candidateIsSynchronized && !hasAccessToAllModels) {
return 0;
}
let result = 0;
if (scheme) {
if (scheme === uriScheme) {
result = 10;
} else if (scheme === '*') {
result = 5;
} else {
return 0;
}
}
if (language) {
if (language === candidateLanguage) {
result = 10;
} else if (language === '*') {
result = Math.max(result, 5);
} else {
return 0;
}
}
if (pattern) {
if (pattern === path || matchGlobPattern(pattern, path)) {
result = 10;
} else {
return 0;
}
}
return result;
} else {
return 0;
}
}

View File

@@ -0,0 +1,22 @@
// *****************************************************************************
// Copyright (C) 2025 STMicroelectronics
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { ContainerModule } from '@theia/core/shared/inversify';
import { bindEditorPreferences } from '../common/editor-preferences';
export default new ContainerModule(bind => {
bindEditorPreferences(bind);
});

View File

@@ -0,0 +1,28 @@
// *****************************************************************************
// Copyright (C) 2017 Ericsson and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
/* note: this bogus test file is required so that
we are able to run mocha unit tests on this
package, without having any actual unit tests in it.
This way a coverage report will be generated,
showing 0% coverage, instead of no report.
This file can be removed once we have real unit
tests in place. */
describe('editor package', () => {
it('support code coverage statistics', () => true);
});

View File

@@ -0,0 +1,19 @@
{
"extends": "../../configs/base.tsconfig",
"compilerOptions": {
"composite": true,
"rootDir": "src",
"outDir": "lib"
},
"include": [
"src"
],
"references": [
{
"path": "../core"
},
{
"path": "../variable-resolver"
}
]
}