feature(code/frontend): main page structure tree (#27838)

This commit is contained in:
WangQianliang 2019-01-03 10:41:17 +08:00 committed by GitHub
parent 274ad11caf
commit 1c001af11c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 151 additions and 61 deletions

View file

@ -15,3 +15,6 @@ export interface SymbolsPayload {
export const loadStructure = createAction<string>('LOAD STRUCTURE');
export const loadStructureSuccess = createAction<SymbolsPayload>('LOAD STRUCTURE SUCCESS');
export const loadStructureFailed = createAction<Error>('LOAD STRUCTURE FAILED');
export const openSymbolPath = createAction<string>('OPEN SYMBOL PATH');
export const closeSymbolPath = createAction<string>('CLOSE SYMBOL PATH');

View file

@ -80,7 +80,7 @@ exports[`render correctly 1`] = `
role="button"
>
<span
className="sc-bdVaJa eSuqza"
className="sc-bdVaJa fsxEnI"
/>
<svg
className="euiIcon euiIcon--medium"
@ -111,7 +111,7 @@ exports[`render correctly 1`] = `
role="button"
>
<span
className="sc-bdVaJa eSuqza"
className="sc-bdVaJa fsxEnI"
/>
<svg
className="euiIcon euiIcon--medium"
@ -142,7 +142,7 @@ exports[`render correctly 1`] = `
role="button"
>
<span
className="sc-bwzfXH gzqToY"
className="sc-bwzfXH kVxRUe sc-bdVaJa fsxEnI"
/>
<svg
className="euiIcon euiIcon--medium"
@ -175,7 +175,7 @@ exports[`render correctly 1`] = `
role="button"
>
<span
className="sc-bdVaJa eSuqza"
className="sc-bdVaJa fsxEnI"
/>
<svg
className="euiIcon euiIcon--medium"
@ -241,7 +241,7 @@ exports[`render correctly 1`] = `
role="button"
>
<span
className="sc-bwzfXH gzqToY"
className="sc-bwzfXH kVxRUe sc-bdVaJa fsxEnI"
/>
<svg
className="euiIcon euiIcon--medium"
@ -274,7 +274,7 @@ exports[`render correctly 1`] = `
role="button"
>
<span
className="sc-bdVaJa eSuqza"
className="sc-bdVaJa fsxEnI"
/>
<svg
className="euiIcon euiIcon--medium"
@ -305,7 +305,7 @@ exports[`render correctly 1`] = `
role="button"
>
<span
className="sc-bdVaJa eSuqza"
className="sc-bdVaJa fsxEnI"
/>
<svg
className="euiIcon euiIcon--medium"
@ -375,7 +375,7 @@ exports[`render correctly 1`] = `
role="button"
>
<span
className="sc-bdVaJa eSuqza"
className="sc-bdVaJa fsxEnI"
/>
<svg
className="euiIcon euiIcon--medium"
@ -406,7 +406,7 @@ exports[`render correctly 1`] = `
role="button"
>
<span
className="sc-bdVaJa eSuqza"
className="sc-bdVaJa fsxEnI"
/>
<svg
className="euiIcon euiIcon--medium"
@ -437,7 +437,7 @@ exports[`render correctly 1`] = `
role="button"
>
<span
className="sc-bdVaJa eSuqza"
className="sc-bdVaJa fsxEnI"
/>
<svg
className="euiIcon euiIcon--medium"
@ -468,7 +468,7 @@ exports[`render correctly 1`] = `
role="button"
>
<span
className="sc-bdVaJa eSuqza"
className="sc-bdVaJa fsxEnI"
/>
<svg
className="euiIcon euiIcon--medium"
@ -499,7 +499,7 @@ exports[`render correctly 1`] = `
role="button"
>
<span
className="sc-bdVaJa eSuqza"
className="sc-bdVaJa fsxEnI"
/>
<svg
className="euiIcon euiIcon--medium"

View file

@ -15,29 +15,7 @@ import { FileTree as Tree, FileTreeItemType } from '../../../model';
import { closeTreePath, fetchRepoTree, FetchRepoTreePayload } from '../../actions';
import { EuiSideNavItem, MainRouteParams, PathTypes } from '../../common/types';
import { RootState } from '../../reducers';
const FolderClosedTriangle = styled.span`
display: inline-block;
width: 0;
height: 0;
margin-right: 4px;
border: 6px solid transparent;
border-left: 6px solid grey;
border-right: 6px solid transparent;
vertical-align: middle;
`;
const FolderOpenTriangle = styled.span`
display: inline-block;
width: 0;
height: 0;
margin-right: 4px;
margin-top: 3px;
border: 6px solid transparent;
border-top: 6px solid grey;
border-bottom: none;
vertical-align: middle;
`;
import { FolderClosedTriangle, FolderOpenTriangle } from '../shared';
const DirectoryNode = styled.span`
margin-left: 4px;

View file

@ -44,7 +44,12 @@ class CodeSideTabs extends React.PureComponent<RouteComponentProps<MainRoutePara
};
public get sideTab(): Tabs {
const tab = parseQuery(this.props.location.search).tab;
const { search } = this.props.location;
let qs = search;
if (search.charAt(0) === '?') {
qs = search.substr(1);
}
const tab = parseQuery(qs).tab;
return tab === Tabs.structure ? Tabs.structure : Tabs.file;
}

View file

@ -0,0 +1,23 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import styled from 'styled-components';
export const FolderClosedTriangle = styled.span`
display: inline-block;
width: 0;
height: 0;
margin-right: 4px;
border: 6px solid transparent;
border-left: 6px solid grey;
vertical-align: middle;
`;
export const FolderOpenTriangle = styled(FolderClosedTriangle)`
margin-top: 3px;
border-top: 6px solid grey;
border-left: 6px solid transparent;
`;

View file

@ -1,3 +0,0 @@
.symbolLinkContainer {
padding: 0.5rem 0;
}

View file

@ -4,19 +4,38 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { EuiSideNav } from '@elastic/eui';
import { EuiFlexGroup, EuiSideNav, EuiText, EuiToken } from '@elastic/eui';
import React from 'react';
import { connect } from 'react-redux';
import { Link } from 'react-router-dom';
import { Location } from 'vscode-languageserver-types/lib/esm/main';
import styled from 'styled-components';
import { Location, SymbolKind } from 'vscode-languageserver-types/lib/esm/main';
import { RepositoryUtils } from '../../../common/repository_utils';
import { closeSymbolPath, openSymbolPath } from '../../actions';
import { EuiSideNavItem } from '../../common/types';
import { RootState } from '../../reducers';
import { SymbolWithMembers } from '../../reducers/symbol';
import { structureSelector } from '../../selectors';
import { FolderClosedTriangle, FolderOpenTriangle } from '../shared';
const Root = styled(EuiSideNav)`
padding: 20px 16px;
overflow: auto;
`;
const Symbol = styled(EuiFlexGroup)`
margin-bottom: 8px;
`;
const Token = styled(EuiToken)`
margin-right: 6px;
`;
interface Props {
structureTree: SymbolWithMembers[];
openPaths: string[];
openSymbolPath: (p: string) => void;
closeSymbolPath: (p: string) => void;
}
const sortSymbol = (a: SymbolWithMembers, b: SymbolWithMembers) => {
@ -28,10 +47,30 @@ const sortSymbol = (a: SymbolWithMembers, b: SymbolWithMembers) => {
}
};
class CodeSymbolTree extends React.PureComponent<Props> {
public getStructureTreeItemRenderer = (location: Location, name: string) => () => (
<div className="symbolLinkContainer">
<Link to={RepositoryUtils.locationToUrl(location)}>{name}</Link>
</div>
public getStructureTreeItemRenderer = (
location: Location,
name: string,
kind: SymbolKind,
isContainer: boolean = false,
forceOpen: boolean = false,
path: string = ''
) => () => (
<Symbol gutterSize="none" alignItems="center">
{isContainer &&
(forceOpen ? (
<FolderOpenTriangle onClick={() => this.props.closeSymbolPath(path)} />
) : (
<FolderClosedTriangle onClick={() => this.props.openSymbolPath(path)} />
))}
{/*
// @ts-ignore */}
<Token iconType={`token${Object.keys(SymbolKind).find(k => SymbolKind[k] === kind)}`} />
<Link to={RepositoryUtils.locationToUrl(location)}>
<EuiText>
<strong>{name}</strong>
</EuiText>
</Link>
</Symbol>
);
public symbolsToSideNavItems = (symbolsWithMembers: SymbolWithMembers[]): EuiSideNavItem[] => {
@ -39,12 +78,27 @@ class CodeSymbolTree extends React.PureComponent<Props> {
const item: EuiSideNavItem = {
name: s.name,
id: `${s.name}_${index}`,
renderItem: this.getStructureTreeItemRenderer(s.location, s.name),
onClick: () => void 0,
};
if (s.members) {
item.items = this.symbolsToSideNavItems(Array.from(s.members));
item.forceOpen = true;
item.forceOpen = this.props.openPaths.includes(s.path!);
item.renderItem = this.getStructureTreeItemRenderer(
s.location,
s.name,
s.kind,
true,
item.forceOpen,
s.path
);
} else {
item.renderItem = this.getStructureTreeItemRenderer(
s.location,
s.name,
s.kind,
false,
false
);
}
return item;
});
@ -54,12 +108,21 @@ class CodeSymbolTree extends React.PureComponent<Props> {
const items = [
{ name: '', id: '', items: this.symbolsToSideNavItems(this.props.structureTree) },
];
return <EuiSideNav items={items} style={{ overflow: 'auto' }} className="sideNavTree" />;
return <Root items={items} />;
}
}
const mapStateToProps = (state: RootState) => {
return { structureTree: structureSelector(state) };
const mapStateToProps = (state: RootState) => ({
structureTree: structureSelector(state),
openPaths: state.symbol.openPaths,
});
const mapDispatchToProps = {
openSymbolPath,
closeSymbolPath,
};
export const SymbolTree = connect(mapStateToProps)(CodeSymbolTree);
export const SymbolTree = connect(
mapStateToProps,
mapDispatchToProps
)(CodeSymbolTree);

View file

@ -10,9 +10,11 @@ import { Action, handleActions } from 'redux-actions';
import { Location, SymbolInformation } from 'vscode-languageserver-types/lib/esm/main';
import {
closeSymbolPath,
loadStructure,
loadStructureFailed,
loadStructureSuccess,
openSymbolPath,
SymbolsPayload,
} from '../actions';
@ -20,7 +22,8 @@ const SPECIAL_SYMBOL_NAME = '{...}';
const SPECIAL_CONTAINER_NAME = '';
export interface SymbolWithMembers extends SymbolInformation {
members?: Set<SymbolInformation>;
members?: Set<SymbolWithMembers>;
path?: string;
}
type Container = SymbolWithMembers | undefined;
@ -31,12 +34,14 @@ export interface SymbolState {
error?: Error;
loading: boolean;
lastRequestPath?: string;
openPaths: string[];
}
const initialState: SymbolState = {
symbols: {},
loading: false,
structureTree: {},
openPaths: [],
};
const generateStructureTree: (symbols: SymbolInformation[]) => any = symbols => {
@ -48,13 +53,13 @@ const generateStructureTree: (symbols: SymbolInformation[]) => any = symbols =>
character: oneLocationStartCharacter,
} = oneLocation.range.start;
const {
line: anotherLocationEndLine,
character: anotherLocationEndCharacter,
} = anotherLocation.range.end;
line: anotherLocationStartLine,
character: anotherLocationStartCharacter,
} = anotherLocation.range.start;
return (
oneLocationStartLine > anotherLocationEndLine ||
(oneLocationStartLine === anotherLocationEndLine &&
oneLocationStartCharacter >= anotherLocationEndCharacter)
oneLocationStartLine > anotherLocationStartLine ||
(oneLocationStartLine === anotherLocationStartLine &&
oneLocationStartCharacter >= anotherLocationStartCharacter)
);
}
@ -79,10 +84,13 @@ const generateStructureTree: (symbols: SymbolInformation[]) => any = symbols =>
container = findContainer(s.containerName || '', s.location);
}
if (container) {
if (!container.path) {
container.path = container.name;
}
if (container.members) {
container.members.add(s);
container.members.add({ ...s, path: `${container.path}/${s.name}` });
} else {
container.members = new Set([s]);
container.members = new Set([{ ...s, path: `${container.path}/${s.name}` }]);
}
} else {
structureTree.push(s);
@ -119,6 +127,20 @@ export const symbol = handleActions(
return state;
}
},
[String(openSymbolPath)]: (state: SymbolState, action: any) =>
produce<SymbolState>(state, draft => {
const path = action.payload!;
if (!state.openPaths.includes(path)) {
draft.openPaths.push(path);
}
}),
[String(closeSymbolPath)]: (state: SymbolState, action: any) =>
produce<SymbolState>(state, draft => {
const idx = state.openPaths.indexOf(action.payload!);
if (idx >= 0) {
draft.openPaths.splice(idx, 1);
}
}),
},
initialState
);

View file

@ -4,7 +4,6 @@
@import "./components/file_tree/file_tree.scss";
@import "./components/file_tree/override_eui_side_nav_styles.scss";
@import "./components/admin_page/admin.scss";
@import "./components/symbol_tree/symbol_tree.scss";
@import "./components/editor/references_panel.scss";
@import "./monaco/monaco.scss";
@import "./monaco/override_monaco_styles.scss";