deploy: current vibn theia state
Made-with: Cursor
This commit is contained in:
10
packages/editor/.eslintrc.js
Normal file
10
packages/editor/.eslintrc.js
Normal 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
31
packages/editor/README.md
Normal 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>
|
||||
51
packages/editor/package.json
Normal file
51
packages/editor/package.json
Normal 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"
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
140
packages/editor/src/browser/decorations/editor-decoration.ts
Normal file
140
packages/editor/src/browser/decorations/editor-decoration.ts
Normal 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,
|
||||
}
|
||||
36
packages/editor/src/browser/decorations/editor-decorator.ts
Normal file
36
packages/editor/src/browser/decorations/editor-decorator.ts
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
19
packages/editor/src/browser/decorations/index.ts
Normal file
19
packages/editor/src/browser/decorations/index.ts
Normal 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';
|
||||
27
packages/editor/src/browser/diff-navigator.ts
Normal file
27
packages/editor/src/browser/diff-navigator.ts
Normal 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;
|
||||
394
packages/editor/src/browser/editor-command.ts
Normal file
394
packages/editor/src/browser/editor-command.ts
Normal 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');
|
||||
}
|
||||
}
|
||||
223
packages/editor/src/browser/editor-contribution.ts
Normal file
223
packages/editor/src/browser/editor-contribution.ts
Normal 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',
|
||||
});
|
||||
}
|
||||
}
|
||||
103
packages/editor/src/browser/editor-frontend-module.ts
Normal file
103
packages/editor/src/browser/editor-frontend-module.ts
Normal 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();
|
||||
});
|
||||
55
packages/editor/src/browser/editor-keybinding.ts
Normal file
55
packages/editor/src/browser/editor-keybinding.ts
Normal 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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
505
packages/editor/src/browser/editor-manager.ts
Normal file
505
packages/editor/src/browser/editor-manager.ts
Normal 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';
|
||||
}
|
||||
227
packages/editor/src/browser/editor-menu.ts
Normal file
227
packages/editor/src/browser/editor-menu.ts
Normal 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'));
|
||||
}
|
||||
|
||||
}
|
||||
332
packages/editor/src/browser/editor-navigation-contribution.ts
Normal file
332
packages/editor/src/browser/editor-navigation-contribution.ts
Normal 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');
|
||||
}
|
||||
}
|
||||
62
packages/editor/src/browser/editor-variable-contribution.ts
Normal file
62
packages/editor/src/browser/editor-variable-contribution.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
82
packages/editor/src/browser/editor-widget-factory.ts
Normal file
82
packages/editor/src/browser/editor-widget-factory.ts
Normal 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';
|
||||
}
|
||||
}
|
||||
146
packages/editor/src/browser/editor-widget.ts
Normal file
146
packages/editor/src/browser/editor-widget.ts
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
378
packages/editor/src/browser/editor.ts
Normal file
378
packages/editor/src/browser/editor.ts
Normal 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>;
|
||||
}
|
||||
27
packages/editor/src/browser/index.ts
Normal file
27
packages/editor/src/browser/index.ts
Normal 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';
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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: {} };
|
||||
}
|
||||
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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.
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
418
packages/editor/src/browser/navigation/navigation-location.ts
Normal file
418
packages/editor/src/browser/navigation/navigation-location.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
94
packages/editor/src/browser/quick-editor-service.ts
Normal file
94
packages/editor/src/browser/quick-editor-service.ts
Normal 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),
|
||||
};
|
||||
}
|
||||
}
|
||||
46
packages/editor/src/browser/split-editor-contribution.ts
Normal file
46
packages/editor/src/browser/split-editor-contribution.ts
Normal 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>;
|
||||
}
|
||||
|
||||
20
packages/editor/src/browser/style/index.css
Normal file
20
packages/editor/src/browser/style/index.css
Normal 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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
120
packages/editor/src/browser/undo-redo-service.ts
Normal file
120
packages/editor/src/browser/undo-redo-service.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
3197
packages/editor/src/common/editor-generated-preference-schema.ts
Normal file
3197
packages/editor/src/common/editor-generated-preference-schema.ts
Normal file
File diff suppressed because it is too large
Load Diff
229
packages/editor/src/common/editor-preferences.ts
Normal file
229
packages/editor/src/common/editor-preferences.ts
Normal 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);
|
||||
}
|
||||
104
packages/editor/src/common/language-selector.ts
Normal file
104
packages/editor/src/common/language-selector.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
22
packages/editor/src/node/editor-backend-module.ts
Normal file
22
packages/editor/src/node/editor-backend-module.ts
Normal 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);
|
||||
});
|
||||
28
packages/editor/src/package.spec.ts
Normal file
28
packages/editor/src/package.spec.ts
Normal 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);
|
||||
});
|
||||
19
packages/editor/tsconfig.json
Normal file
19
packages/editor/tsconfig.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"extends": "../../configs/base.tsconfig",
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"rootDir": "src",
|
||||
"outDir": "lib"
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../core"
|
||||
},
|
||||
{
|
||||
"path": "../variable-resolver"
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user