Overhaul the query history tab to allow browsing of the history and full query text. Fixes #2282

Patch by Joao and the team at Pivotal.
This commit is contained in:
Joao Pedro De Almeida Pereira 2017-06-27 10:55:57 -04:00 committed by Dave Page
parent e413186d23
commit 7f55412059
63 changed files with 10645 additions and 8977 deletions

View File

@ -38,6 +38,7 @@
"moment": "^2.18.1",
"react": "file:../web/pgadmin/static/vendor/react",
"react-dom": "file:../web/pgadmin/static/vendor/react-dom",
"react-split-pane": "^0.1.63",
"requirejs": "~2.3.3",
"slickgrid": "git+https://github.com/6pac/SlickGrid.git#2.3.7",
"underscore": "~1.8.3",

View File

@ -75,10 +75,18 @@ class QueryToolJourneyTest(BaseFeatureTest):
self._execute_query("SELECT * FROM shoes")
self.page.click_tab("History")
history_element = self.page.find_by_id("history_grid")
history_element = self.page.find_by_id("query_list")
self.assertIn("SELECT * FROM test_table", history_element.text)
self.assertIn("SELECT * FROM shoes", history_element.text)
history_detail = self.page.find_by_id("query_detail")
self.assertIn("SELECT * FROM shoes", history_detail.text)
old_history_list_element = self.page.find_by_xpath("//*[@id='query_list']/ul/li[2]")
self.page.click_element(old_history_list_element)
history_detail = self.page.find_by_id("query_detail")
self.assertIn("SELECT * FROM test_table", history_detail.text)
def __clear_query_tool(self):
self.page.click_element(self.page.find_by_xpath("//*[@id='btn-edit']"))
self.page.click_modal('Yes')

View File

@ -55,12 +55,6 @@ iframe {
border-width: 0;
}
/* Ensure the codemirror editor displays full height gutters when resized */
.CodeMirror, .CodeMirror-gutter {
height: 100% !important;
min-height: 100% !important;
}
/* Padding for the treeview */
.browser-browser-pane {
padding-left: 0;
@ -926,22 +920,6 @@ td.edit-cell.editable.sortable.renderable.editor {
padding: 0px;
}
/*
* Remove border right from "CodeMirror-gutters"
* class becuase its height doesn't increase
* with every new line added, instead add border
* right to ".CodeMirror-linenumber" class which
* adds a new div for every new line added
*/
.sql_field_layout .CodeMirror-gutters {
border-right: none;
}
.sql_field_layout .CodeMirror-linenumber {
border-right: 1px solid #ddd;
background-color: #f7f7f7;
}
.tab-pane.SQL {
padding: 0px 7px 0px 0px;
height: 100%;
@ -1420,22 +1398,6 @@ table.backgrid {
background: none;
}
/* class to disable Codemirror editor */
.cm_disabled {
background: #EEEEEE;
}
/* make syntax-highlighting bold */
.cm-s-default .cm-keyword {
font-weight: 600;
color: #908;
}
.cm-s-default .cm-number {
font-weight: 600;
color: #964;
}
.pgadmin-controls.sql_field_layout {
margin: 0;
padding: 0;
@ -1443,10 +1405,6 @@ table.backgrid {
z-index: 1;
}
.pgadmin-controls.sql_field_layout .CodeMirror {
line-height: 22px;
}
.pgadmin-controls.focused {
border-color: #66afe9;
outline: 0;
@ -1502,22 +1460,6 @@ body {
user-select: text;
}
/* Codemirror buttons */
.CodeMirror-dialog button {
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
color: white;
font-size: 70%;
background-image: -webkit-linear-gradient(top, #337ab7 0%, #265a88 100%);
background-image: -o-linear-gradient(top, #337ab7 0%, #265a88 100%);
background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#265a88));
background-image: linear-gradient(to bottom, #337ab7 0%, #265a88 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff265a88', GradientType=0);
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
background-repeat: repeat-x;
border-color: #245580;
border-radius: 4px;
}
.dashboard-tab-container table,
.pg-panel-statistics-container table,
.pg-panel-depends-container table,

View File

@ -3,3 +3,69 @@
color: #333333 !important;
background-color: #e8e8e8 !important;
}
/* Ensure the codemirror editor displays full height gutters when resized */
.CodeMirror, .CodeMirror-gutter {
height: 100% !important;
min-height: 100% !important;
}
/*
* Remove border right from "CodeMirror-gutters"
* class becuase its height doesn't increase
* with every new line added, instead add border
* right to ".CodeMirror-linenumber" class which
* adds a new div for every new line added
*/
.sql_field_layout .CodeMirror-gutters {
border-right: none;
}
.sql_field_layout .CodeMirror-linenumber {
border-right: 1px solid #ddd;
background-color: #f7f7f7;
}
/* class to disable Codemirror editor */
.cm_disabled {
background: #EEEEEE;
}
/* make syntax-highlighting bold */
.cm-s-default .cm-keyword {
font-weight: 600;
color: #908;
}
.cm-s-default .cm-number {
font-weight: 600;
color: #964;
}
.pgadmin-controls.sql_field_layout .CodeMirror {
line-height: 22px;
}
/* Codemirror buttons */
.CodeMirror-dialog button {
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
color: white;
font-size: 70%;
background-image: -webkit-linear-gradient(top, #337ab7 0%, #265a88 100%);
background-image: -o-linear-gradient(top, #337ab7 0%, #265a88 100%);
background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#265a88));
background-image: linear-gradient(to bottom, #337ab7 0%, #265a88 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff265a88', GradientType=0);
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
background-repeat: repeat-x;
border-color: #245580;
border-radius: 4px;
}
#history-detail-query .CodeMirror {
border: 1px solid #CCCCCC;
background-color: #F7F7F7;
width: 100%;
padding-left: 5px;
position: absolute;
}

View File

@ -781,3 +781,35 @@ lgg-el-container[el=md] .pg-el-lg-8,
.user-language select{
height: 25px !important;
}
.Resizer {
background: #dddddd;
z-index: 1;
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
box-sizing: border-box;
-moz-background-clip: padding;
-webkit-background-clip: padding;
background-clip: padding-box;
}
.Resizer.horizontal {
width: 100%;
height: 4px;
cursor: ew-resize;
border-bottom: 1px solid #aaaaaa;
}
.Resizer.vertical {
width: 4px;
cursor: ew-resize;
border-right: 1px solid #aaaaaa;
}
.Resizer.disabled {
cursor: not-allowed;
}
.Resizer.disabled:hover {
border-color: transparent;
}

View File

@ -7,8 +7,8 @@
//
//////////////////////////////////////////////////////////////
import historyCollection from './history_collection';
import HistoryCollection from './history_collection';
export {
historyCollection,
HistoryCollection,
};

View File

@ -0,0 +1,61 @@
//////////////////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2017, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////////////////
import React from 'react';
import $ from '../../../vendor/jquery/jquery-1.11.2';
import code_mirror from '../../../vendor/codemirror/lib/codemirror';
export default class CodeMirror extends React.Component {
constructor(props) {
super(props);
this.state = {
shouldHydrate: true,
};
}
componentDidMount() {
this.editor = code_mirror(this.container);
this.hydrateInterval = setInterval(this.hydrateWhenBecomesVisible.bind(this), 100);
this.hydrate(this.props);
}
componentWillUnmount() {
clearInterval(this.hydrateInterval);
}
componentWillReceiveProps(nextProps) {
this.hydrate(nextProps);
}
hydrateWhenBecomesVisible() {
const isVisible = $(this.container).is(':visible');
if (isVisible && this.state.shouldHydrate) {
this.hydrate(this.props);
this.setState({shouldHydrate: false});
} else if (!isVisible) {
this.setState({shouldHydrate: true});
}
}
hydrate(props) {
Object.keys(props.options || {}).forEach(key => this.editor.setOption(key, props.options[key]));
this.editor.setValue(props.value || '');
this.editor.refresh();
}
render() {
return (
<div ref={(self) => this.container = self}/>
);
}
}

View File

@ -0,0 +1,60 @@
//////////////////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2017, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////////////////
import React from 'react';
import Shapes from '../../react_shapes';
import NonSelectableElementStyle from '../../styles/non_selectable';
import MessageHeaderStyle from '../../styles/header_label';
const containerStyle = {
flex: '2 2 0%',
flexDirection: 'column',
display: 'flex',
};
const messageContainerStyle = {
flex: '0 1 auto',
overflow: 'auto',
position: 'relative',
height: '100%',
};
const errorMessageStyle = {
border: '0',
paddingLeft: '0',
backgroundColor: '#ffffff',
fontSize: '13px',
position: 'absolute',
};
const messageLabelStyle = _.extend({flex: '0 0 auto'},
MessageHeaderStyle,
NonSelectableElementStyle);
export default class HistoryDetailMessage extends React.Component {
render() {
return (
<div style={containerStyle}>
<div style={messageLabelStyle}>
Messages
</div>
<div style={messageContainerStyle}>
<pre style={errorMessageStyle}>
{this.props.historyEntry.message}
</pre>
</div>
</div>);
}
}
HistoryDetailMessage.propTypes = {
historyEntry: Shapes.historyDetail,
};

View File

@ -0,0 +1,54 @@
//////////////////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2017, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////////////////
import React from 'react';
import moment from 'moment';
import Shapes from '../../react_shapes';
import HeaderDescriptionStyle from '../../styles/header_label';
const queryMetaDataStyle = {
flex: 1,
};
const headerStyle = {
display: 'flex',
};
const headerValueStyle = {
display: 'block',
fontSize: '14px',
};
export default class HistoryDetailMetadata extends React.Component {
formatDate(date) {
return (moment(date).format('M-D-YY HH:mm:ss'));
}
queryMetaData(data, description) {
return <div style={queryMetaDataStyle}>
<span style={headerValueStyle}>
{data}
</span>
<span style={HeaderDescriptionStyle}>
{description}
</span>
</div>;
}
render() {
return <div style={headerStyle}>
{this.queryMetaData(this.formatDate(this.props.historyEntry.start_time), 'Date')}
{this.queryMetaData(this.props.historyEntry.row_affected.toLocaleString(), 'Rows Affected')}
{this.queryMetaData(this.props.historyEntry.total_time, 'Duration')}
</div>;
}
}
HistoryDetailMetadata.propTypes = {
historyEntry: Shapes.historyDetail,
};

View File

@ -0,0 +1,33 @@
//////////////////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2017, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////////////////
import React from 'react';
import '../../../vendor/codemirror/mode/sql/sql';
import CodeMirror from './code_mirror';
import Shapes from '../../react_shapes';
export default class HistoryDetailQuery extends React.Component {
render() {
return (
<div id="history-detail-query">
<CodeMirror
value={this.props.historyEntry.query}
options={{
mode: 'text/x-pgsql',
readOnly: true,
}}
/>
</div>);
}
}
HistoryDetailQuery.propTypes = {
historyEntry: Shapes.historyDetail,
};

View File

@ -0,0 +1,20 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2017, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import QueryHistoryVanillaEntry from './query_history_vanilla_entry';
import update from 'immutability-helper';
import {errorStyle} from '../../styles/history_entry_styles';
export default class QueryHistoryErrorEntry extends QueryHistoryVanillaEntry {
componentWillMount() {
this.setState({
outerDivStyle: update(this.state.outerDivStyle, {$merge: errorStyle}),
});
}
}

View File

@ -0,0 +1,21 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2017, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import QueryHistoryVanillaEntry from './query_history_vanilla_entry';
import update from 'immutability-helper';
import {selectedFontStyle, selectedOuterStyle} from '../../styles/history_entry_styles';
export default class QueryHistorySelectedEntry extends QueryHistoryVanillaEntry {
componentWillMount() {
this.setState({
outerDivStyle: update(this.state.outerDivStyle, {$merge: selectedOuterStyle}),
secondLineStyle: update(this.state.secondLineStyle, {$merge: selectedFontStyle}),
});
}
}

View File

@ -0,0 +1,22 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2017, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import QueryHistoryVanillaEntry from './query_history_vanilla_entry';
import update from 'immutability-helper';
import {selectedFontStyle, selectedOuterStyle, selectedErrorBgColor} from '../../styles/history_entry_styles';
export default class QueryHistorySelectedErrorEntry extends QueryHistoryVanillaEntry {
componentWillMount() {
let selectedErrorStyle = update(selectedOuterStyle, {$merge: selectedErrorBgColor});
this.setState({
outerDivStyle: update(this.state.outerDivStyle, {$merge: selectedErrorStyle}),
secondLineStyle: update(this.state.secondLineStyle, {$merge: selectedFontStyle}),
});
}
}

View File

@ -0,0 +1,47 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2017, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import React from 'react';
import moment from 'moment';
import Shapes from '../../react_shapes';
import {plainOuterDivStyle, plainSecondLineStyle, sqlStyle, timestampStyle} from '../../styles/history_entry_styles';
export default class QueryHistoryVanillaEntry extends React.Component {
formatDate(date) {
return (moment(date).format('MMM D YYYY [] HH:mm:ss'));
}
constructor(props) {
super(props);
this.state = {
outerDivStyle: plainOuterDivStyle,
secondLineStyle: plainSecondLineStyle,
};
}
render() {
return (
<div style={this.state.outerDivStyle}>
<div style={sqlStyle}>
{this.props.historyEntry.query}
</div>
<div style={this.state.secondLineStyle}>
<div style={timestampStyle}>
{this.formatDate(this.props.historyEntry.start_time)}
</div>
</div>
</div>
);
}
}
QueryHistoryVanillaEntry.propTypes = {
historyEntry: Shapes.historyDetail,
isSelected: React.PropTypes.bool,
};

View File

@ -8,8 +8,17 @@
//////////////////////////////////////////////////////////////
import React from 'react';
import SplitPane from 'react-split-pane';
import QueryHistoryEntry from './query_history_entry';
import QueryHistoryDetail from './query_history_detail';
import Shapes from '../react_shapes';
const queryEntryListDivStyle = {
overflowY: 'auto',
};
const queryDetailDivStyle = {
display: 'flex',
};
const liStyle = {
borderBottom: '1px solid #cccccc',
};
@ -21,29 +30,67 @@ export default class QueryHistory extends React.Component {
this.state = {
history: [],
selectedEntry: 0,
};
}
componentWillMount() {
this.setState({history: this.props.historyCollection.historyList});
this.props.historyCollection.onChange((historyList) => this.setState({history: historyList}));
this.resetCurrentHistoryDetail(this.props.historyCollection.historyList);
this.props.historyCollection.onChange((historyList) => {
this.resetCurrentHistoryDetail(historyList);
});
}
componentDidMount() {
this.resetCurrentHistoryDetail(this.state.history);
}
getCurrentHistoryDetail() {
return this.state.currentHistoryDetail;
}
setCurrentHistoryDetail(index, historyList) {
this.setState({
history: historyList,
currentHistoryDetail: this.retrieveOrderedHistory().value()[index],
selectedEntry: index,
});
}
resetCurrentHistoryDetail(historyList) {
this.setCurrentHistoryDetail(0, historyList);
}
retrieveOrderedHistory() {
return _.chain(this.state.history)
.sortBy(historyEntry => historyEntry.start_time)
.reverse();
}
onClickHandler(index) {
this.setCurrentHistoryDetail(index, this.state.history);
}
render() {
return <ul>
{_.chain(this.state.history)
.sortBy(historyEntry => historyEntry.start_time)
.reverse()
.map((entry, index) =>
<li key={index} style={liStyle}>
<QueryHistoryEntry historyEntry={entry}/>
</li>)
.value()
}
</ul>;
return (
<SplitPane defaultSize="50%" split="vertical" pane1Style={queryEntryListDivStyle}
pane2Style={queryDetailDivStyle}>
<div id='query_list'>
<ul>
{this.retrieveOrderedHistory()
.map((entry, index) =>
<li key={index} style={liStyle} onClick={this.onClickHandler.bind(this, index)}>
<QueryHistoryEntry historyEntry={entry} isSelected={index == this.state.selectedEntry}/>
</li>)
.value()
}
</ul>
</div>
<QueryHistoryDetail historyEntry={this.getCurrentHistoryDetail()}/>
</SplitPane>);
}
}
QueryHistory.propTypes = {
historyCollection: React.PropTypes.object.isRequired,
historyCollection: Shapes.historyCollectionClass.isRequired,
};

View File

@ -0,0 +1,75 @@
//////////////////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2017, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////////////////
import React from 'react';
import HistoryDetailMetadata from './detail/history_detail_metadata';
import HistoryDetailQuery from './detail/history_detail_query';
import HistoryDetailMessage from './detail/history_detail_message';
import Shapes from '../react_shapes';
const outerStyle = {
width: '100%',
paddingTop: '10px',
display: 'flex',
flexDirection: 'column',
};
const detailVerticalTop = {
flex: 1,
padding: '0 10px',
};
const detailVerticalMiddle = {
flex: 5,
marginLeft: '10px',
marginRight: '10px',
height: 0,
position: 'relative',
};
const hrStyle = {
borderColor: '#cccccc',
marginTop: '11px',
marginBottom: '8px',
};
const detailVerticalBottom = {
flex: 2,
display: 'flex',
paddingLeft: '10px',
};
export default class QueryHistoryDetail extends React.Component {
render() {
if (!_.isUndefined(this.props.historyEntry)) {
return (
<div id='query_detail' style={outerStyle}>
<div style={detailVerticalTop}>
<HistoryDetailMetadata {...this.props} />
</div>
<div style={detailVerticalMiddle}>
<HistoryDetailQuery {...this.props}/>
</div>
<div>
<hr style={hrStyle}/>
</div>
<div style={detailVerticalBottom}>
<HistoryDetailMessage {...this.props}/>
</div>
</div>);
} else {
return <p></p>;
}
}
}
QueryHistoryDetail.propTypes = {
historyEntry: Shapes.historyDetail,
};

View File

@ -1,93 +1,42 @@
/////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2017, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////
import React from 'react';
import update from 'immutability-helper';
import moment from 'moment';
const outerDivStyle = {
paddingLeft: '10px',
fontFamily: 'monospace',
paddingRight: '20px',
fontSize: '14px',
backgroundColor: '#FFF',
};
const sqlStyle = {
textOverflow: 'ellipsis',
overflow: 'hidden',
whiteSpace: 'nowrap',
userSelect: 'auto',
};
const secondLineStyle = {
display: 'flex',
flexDirection: 'row',
justifyContent: 'space-between',
fontSize: '13px',
color: '#888888',
};
const timestampStyle = {
alignSelf: 'flex-start',
};
const rowsAffectedStyle = {
alignSelf: 'flex-end',
};
const errorMessageStyle = {
textOverflow: 'ellipsis',
overflow: 'hidden',
whiteSpace: 'nowrap',
userSelect: 'auto',
fontSize: '13px',
color: '#888888',
};
import Shapes from '../react_shapes';
import QueryHistoryErrorEntry from './entry/query_history_error_entry';
import QueryHistorySelectedErrorEntry from './entry/query_history_selected_error_entry';
import QueryHistorySelectedEntry from './entry/query_history_selected_entry';
import QueryHistoryVanillaEntry from './entry/query_history_vanilla_entry';
export default class QueryHistoryEntry extends React.Component {
formatDate(date) {
return (moment(date).format('MMM D YYYY [] HH:mm:ss'));
}
render() {
return (
<div style={this.queryEntryBackgroundColor()}>
<div style={sqlStyle}>
{this.props.historyEntry.query}
</div>
<div style={secondLineStyle}>
<div style={timestampStyle}>
{this.formatDate(this.props.historyEntry.start_time)} /
total time: {this.props.historyEntry.total_time}
</div>
<div style={rowsAffectedStyle}>
{this.props.historyEntry.row_affected} rows affected
</div>
</div>
<div style={errorMessageStyle}>
{this.props.historyEntry.message}
</div>
</div>
);
if (this.hasError()) {
if (this.props.isSelected) {
return <QueryHistorySelectedErrorEntry {...this.props}/>;
} else {
return <QueryHistoryErrorEntry {...this.props}/>;
}
} else {
if (this.props.isSelected) {
return <QueryHistorySelectedEntry {...this.props}/>;
} else {
return <QueryHistoryVanillaEntry {...this.props}/>;
}
}
}
queryEntryBackgroundColor() {
if (!this.props.historyEntry.status) {
return update(outerDivStyle, {$merge: {backgroundColor: '#F7D0D5'}});
}
return outerDivStyle;
hasError() {
return !this.props.historyEntry.status;
}
}
QueryHistoryEntry.propTypes = {
historyEntry: React.PropTypes.shape({
query: React.PropTypes.string,
start_time: React.PropTypes.instanceOf(Date),
status: React.PropTypes.bool,
total_time: React.PropTypes.string,
row_affected: React.PropTypes.int,
message: React.PropTypes.string,
}),
historyEntry: Shapes.historyDetail,
isSelected: React.PropTypes.bool,
};

View File

@ -0,0 +1,31 @@
//////////////////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2017, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////////////////
import React from 'react';
let historyDetail =
React.PropTypes.shape({
query: React.PropTypes.string,
start_time: React.PropTypes.instanceOf(Date),
status: React.PropTypes.bool,
total_time: React.PropTypes.string,
row_affected: React.PropTypes.int,
message: React.PropTypes.string,
});
let historyCollectionClass =
React.PropTypes.shape({
historyList: React.PropTypes.array.isRequired,
onChange: React.PropTypes.func.isRequired,
});
export default {
historyDetail,
historyCollectionClass,
};

View File

@ -0,0 +1,14 @@
//////////////////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2017, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////////////////
export default {
display: 'block',
fontSize: '12px',
color: '#888888',
};

View File

@ -0,0 +1,57 @@
//////////////////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2017, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////////////////
import update from 'immutability-helper';
export const plainOuterDivStyle = {
paddingLeft: '8px',
paddingRight: '18px',
paddingTop: '-2px',
paddingBottom: '-2px',
fontFamily: 'monospace',
fontSize: '14px',
backgroundColor: '#FFF',
border: '2px solid transparent',
marginLeft: '1px',
};
export const sqlStyle = {
textOverflow: 'ellipsis',
overflow: 'hidden',
whiteSpace: 'nowrap',
userSelect: 'auto',
};
export const plainSecondLineStyle = {
display: 'flex',
flexDirection: 'row',
justifyContent: 'space-between',
fontSize: '13px',
color: '#888888',
};
export const timestampStyle = {
alignSelf: 'flex-start',
};
export const selectedFontStyle = {
color: '#2c76b4',
fontWeight: 'bold',
};
export const selectedOuterStyle = update(selectedFontStyle, {
$merge: {
border: '2px solid #2c76b4',
backgroundColor: '#e7f2ff',
},
});
export const errorStyle = {backgroundColor: '#F7D0D5'};
export const selectedErrorBgColor = {backgroundColor: '#DCC4D1'};

View File

@ -0,0 +1,18 @@
//////////////////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2017, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////////////////
export default {
WebkitTouchCallout: 'none',
WebkitUserSelect: 'none',
KhtmlUserSelect: 'none',
MozUserSelect: 'none',
msUserSelect: 'none',
userSelect: 'none',
cursor: 'default',
};

View File

@ -46,12 +46,17 @@
// Rough heuristic to try and detect lines that are part of multi-line string
function probablyInsideString(cm, pos, line) {
return /\bstring\b/.test(cm.getTokenTypeAt(Pos(pos.line, 0))) && !/^[\'\"`]/.test(line)
return /\bstring\b/.test(cm.getTokenTypeAt(Pos(pos.line, 0))) && !/^[\'\"\`]/.test(line)
}
function getMode(cm, pos) {
var mode = cm.getMode()
return mode.useInnerComments === false || !mode.innerMode ? mode : cm.getModeAt(pos)
}
CodeMirror.defineExtension("lineComment", function(from, to, options) {
if (!options) options = noOptions;
var self = this, mode = self.getModeAt(from);
var self = this, mode = getMode(self, from);
var firstLine = self.getLine(from.line);
if (firstLine == null || probablyInsideString(self, from, firstLine)) return;
@ -95,7 +100,7 @@
CodeMirror.defineExtension("blockComment", function(from, to, options) {
if (!options) options = noOptions;
var self = this, mode = self.getModeAt(from);
var self = this, mode = getMode(self, from);
var startString = options.blockCommentStart || mode.blockCommentStart;
var endString = options.blockCommentEnd || mode.blockCommentEnd;
if (!startString || !endString) {
@ -103,6 +108,7 @@
self.lineComment(from, to, options);
return;
}
if (/\bcomment\b/.test(self.getTokenTypeAt(Pos(from.line, 0)))) return
var end = Math.min(to.line, self.lastLine());
if (end != from.line && to.ch == 0 && nonWS.test(self.getLine(end))) --end;
@ -128,7 +134,7 @@
CodeMirror.defineExtension("uncomment", function(from, to, options) {
if (!options) options = noOptions;
var self = this, mode = self.getModeAt(from);
var self = this, mode = getMode(self, from);
var end = Math.min(to.ch != 0 || to.line == from.line ? to.line : to.line - 1, self.lastLine()), start = Math.min(from.line, end);
// Try finding line comments
@ -140,7 +146,7 @@
var line = self.getLine(i);
var found = line.indexOf(lineString);
if (found > -1 && !/comment/.test(self.getTokenTypeAt(Pos(i, found + 1)))) found = -1;
if (found == -1 && (i != end || i == start) && nonWS.test(line)) break lineComment;
if (found == -1 && nonWS.test(line)) break lineComment;
if (found > -1 && nonWS.test(line.slice(0, found))) break lineComment;
lines.push(line);
}
@ -162,15 +168,19 @@
var endString = options.blockCommentEnd || mode.blockCommentEnd;
if (!startString || !endString) return false;
var lead = options.blockCommentLead || mode.blockCommentLead;
var startLine = self.getLine(start), endLine = end == start ? startLine : self.getLine(end);
var open = startLine.indexOf(startString), close = endLine.lastIndexOf(endString);
var startLine = self.getLine(start), open = startLine.indexOf(startString)
if (open == -1) return false
var endLine = end == start ? startLine : self.getLine(end)
var close = endLine.indexOf(endString, end == start ? open + startString.length : 0);
if (close == -1 && start != end) {
endLine = self.getLine(--end);
close = endLine.lastIndexOf(endString);
close = endLine.indexOf(endString);
}
if (open == -1 || close == -1 ||
!/comment/.test(self.getTokenTypeAt(Pos(start, open + 1))) ||
!/comment/.test(self.getTokenTypeAt(Pos(end, close + 1))))
var insideStart = Pos(start, open + 1), insideEnd = Pos(end, close + 1)
if (close == -1 ||
!/comment/.test(self.getTokenTypeAt(insideStart)) ||
!/comment/.test(self.getTokenTypeAt(insideEnd)) ||
self.getRange(insideStart, insideEnd, "\n").indexOf(endString) > -1)
return false;
// Avoid killing block comments completely outside the selection.

View File

@ -38,6 +38,9 @@
var height = (options && options.height) || node.offsetHeight;
this._setSize(null, info.heightLeft -= height);
info.panels++;
if (options.stable && isAtTop(this, node))
this.scrollTo(null, this.getScrollInfo().top + height)
return new Panel(this, node, options, height);
});
@ -54,6 +57,8 @@
this.cleared = true;
var info = this.cm.state.panels;
this.cm._setSize(null, info.heightLeft += this.height);
if (this.options.stable && isAtTop(this.cm, this.node))
this.cm.scrollTo(null, this.cm.getScrollInfo().top - this.height)
info.wrapper.removeChild(this.node);
if (--info.panels == 0) removePanels(this.cm);
};
@ -61,7 +66,7 @@
Panel.prototype.changed = function(height) {
var newHeight = height == null ? this.node.offsetHeight : height;
var info = this.cm.state.panels;
this.cm._setSize(null, info.height += (newHeight - this.height));
this.cm._setSize(null, info.heightLeft -= (newHeight - this.height));
this.height = newHeight;
};
@ -109,4 +114,10 @@
cm.setSize = cm._setSize;
cm.setSize();
}
function isAtTop(cm, dom) {
for (var sibling = dom.nextSibling; sibling; sibling = sibling.nextSibling)
if (sibling == cm.getWrapperElement()) return true
return false
}
});

View File

@ -11,30 +11,26 @@
})(function(CodeMirror) {
"use strict";
CodeMirror.defineOption("rulers", false, function(cm, val, old) {
if (old && old != CodeMirror.Init) {
clearRulers(cm);
cm.off("refresh", refreshRulers);
CodeMirror.defineOption("rulers", false, function(cm, val) {
if (cm.state.rulerDiv) {
cm.state.rulerDiv.parentElement.removeChild(cm.state.rulerDiv)
cm.state.rulerDiv = null
cm.off("refresh", drawRulers)
}
if (val && val.length) {
setRulers(cm);
cm.on("refresh", refreshRulers);
cm.state.rulerDiv = cm.display.lineSpace.parentElement.insertBefore(document.createElement("div"), cm.display.lineSpace)
cm.state.rulerDiv.className = "CodeMirror-rulers"
drawRulers(cm)
cm.on("refresh", drawRulers)
}
});
function clearRulers(cm) {
for (var i = cm.display.lineSpace.childNodes.length - 1; i >= 0; i--) {
var node = cm.display.lineSpace.childNodes[i];
if (/(^|\s)CodeMirror-ruler($|\s)/.test(node.className))
node.parentNode.removeChild(node);
}
}
function setRulers(cm) {
function drawRulers(cm) {
cm.state.rulerDiv.textContent = ""
var val = cm.getOption("rulers");
var cw = cm.defaultCharWidth();
var left = cm.charCoords(CodeMirror.Pos(cm.firstLine(), 0), "div").left;
var minH = cm.display.scroller.offsetHeight + 30;
cm.state.rulerDiv.style.minHeight = (cm.display.scroller.offsetHeight + 30) + "px";
for (var i = 0; i < val.length; i++) {
var elt = document.createElement("div");
elt.className = "CodeMirror-ruler";
@ -49,15 +45,7 @@
if (conf.width) elt.style.borderLeftWidth = conf.width;
}
elt.style.left = (left + col * cw) + "px";
elt.style.top = "-50px";
elt.style.bottom = "-20px";
elt.style.minHeight = minH + "px";
cm.display.lineSpace.insertBefore(elt, cm.display.cursorDiv);
cm.state.rulerDiv.appendChild(elt)
}
}
function refreshRulers(cm) {
clearRulers(cm);
setRulers(cm);
}
});

View File

@ -45,7 +45,7 @@
function getConfig(cm) {
var deflt = cm.state.closeBrackets;
if (!deflt) return null;
if (!deflt || deflt.override) return deflt;
var mode = cm.getModeAt(cm.getCursor());
return mode.closeBrackets || deflt;
}
@ -109,14 +109,16 @@
var ranges = cm.listSelections();
var opening = pos % 2 == 0;
var type, next;
var type;
for (var i = 0; i < ranges.length; i++) {
var range = ranges[i], cur = range.head, curType;
var next = cm.getRange(cur, Pos(cur.line, cur.ch + 1));
if (opening && !range.empty()) {
curType = "surround";
} else if ((identical || !opening) && next == ch) {
if (triples.indexOf(ch) >= 0 && cm.getRange(cur, Pos(cur.line, cur.ch + 3)) == ch + ch + ch)
if (identical && stringStartsAfter(cm, cur))
curType = "both";
else if (triples.indexOf(ch) >= 0 && cm.getRange(cur, Pos(cur.line, cur.ch + 3)) == ch + ch + ch)
curType = "skipThree";
else
curType = "skip";
@ -183,7 +185,7 @@
function enteringString(cm, pos, ch) {
var line = cm.getLine(pos.line);
var token = cm.getTokenAt(pos);
if (/\bstring2?\b/.test(token.type)) return false;
if (/\bstring2?\b/.test(token.type) || stringStartsAfter(cm, pos)) return false;
var stream = new CodeMirror.StringStream(line.slice(0, pos.ch) + ch + line.slice(pos.ch), 4);
stream.pos = stream.start = token.start;
for (;;) {
@ -192,4 +194,9 @@
stream.start = stream.pos;
}
}
function stringStartsAfter(cm, pos) {
var token = cm.getTokenAt(Pos(pos.line, pos.ch + 1))
return /\bstring/.test(token.type) && token.start == pos.ch
}
});

View File

@ -11,8 +11,8 @@
})(function(CodeMirror) {
"use strict";
var listRE = /^(\s*)(>[> ]*|[*+-]\s|(\d+)([.)]))(\s*)/,
emptyListRE = /^(\s*)(>[> ]*|[*+-]|(\d+)[.)])(\s*)$/,
var listRE = /^(\s*)(>[> ]*|[*+-] \[[x ]\]\s|[*+-]\s|(\d+)([.)]))(\s*)/,
emptyListRE = /^(\s*)(>[> ]*|[*+-] \[[x ]\]|[*+-]|(\d+)[.)])(\s*)$/,
unorderedListRE = /[*+-]\s/;
CodeMirror.commands.newlineAndIndentContinueMarkdownList = function(cm) {
@ -30,7 +30,7 @@
return;
}
if (emptyListRE.test(line)) {
cm.replaceRange("", {
if (!/>\s*$/.test(line)) cm.replaceRange("", {
line: pos.line, ch: 0
}, {
line: pos.line, ch: pos.ch + 1
@ -39,7 +39,7 @@
} else {
var indent = match[1], after = match[5];
var bullet = unorderedListRE.test(match[2]) || match[2].indexOf(">") >= 0
? match[2]
? match[2].replace("x", " ")
: (parseInt(match[3], 10) + 1) + match[4];
replacements[i] = "\n" + indent + bullet + after;

View File

@ -102,8 +102,10 @@
}
CodeMirror.defineOption("matchBrackets", false, function(cm, val, old) {
if (old && old != CodeMirror.Init)
if (old && old != CodeMirror.Init) {
cm.off("cursorActivity", doMatchBrackets);
if (currentlyHighlighted) {currentlyHighlighted(); currentlyHighlighted = null;}
}
if (val) {
cm.state.matchBrackets = typeof val == "object" ? val : {};
cm.on("cursorActivity", doMatchBrackets);

View File

@ -13,7 +13,7 @@
CodeMirror.registerHelper("fold", "brace", function(cm, start) {
var line = start.line, lineText = cm.getLine(line);
var startCh, tokenType;
var tokenType;
function findOpening(openCh) {
for (var at = start.ch, pass = 0;;) {
@ -72,15 +72,15 @@ CodeMirror.registerHelper("fold", "import", function(cm, start) {
}
}
var start = start.line, has = hasImport(start), prev;
if (!has || hasImport(start - 1) || ((prev = hasImport(start - 2)) && prev.end.line == start - 1))
var startLine = start.line, has = hasImport(startLine), prev;
if (!has || hasImport(startLine - 1) || ((prev = hasImport(startLine - 2)) && prev.end.line == startLine - 1))
return null;
for (var end = has.end;;) {
var next = hasImport(end.line + 1);
if (next == null) break;
end = next.end;
}
return {from: cm.clipPos(CodeMirror.Pos(start, has.startCh + 1)), to: end};
return {from: cm.clipPos(CodeMirror.Pos(startLine, has.startCh + 1)), to: end};
});
CodeMirror.registerHelper("fold", "include", function(cm, start) {
@ -91,14 +91,14 @@ CodeMirror.registerHelper("fold", "include", function(cm, start) {
if (start.type == "meta" && start.string.slice(0, 8) == "#include") return start.start + 8;
}
var start = start.line, has = hasInclude(start);
if (has == null || hasInclude(start - 1) != null) return null;
for (var end = start;;) {
var startLine = start.line, has = hasInclude(startLine);
if (has == null || hasInclude(startLine - 1) != null) return null;
for (var end = startLine;;) {
var next = hasInclude(end + 1);
if (next == null) break;
++end;
}
return {from: CodeMirror.Pos(start, has + 1),
return {from: CodeMirror.Pos(startLine, has + 1),
to: cm.clipPos(CodeMirror.Pos(end))};
});

View File

@ -29,7 +29,7 @@ CodeMirror.registerGlobalHelper("fold", "comment", function(mode) {
}
if (pass == 1 && found < start.ch) return;
if (/comment/.test(cm.getTokenTypeAt(CodeMirror.Pos(line, found + 1))) &&
(lineText.slice(found - endToken.length, found) == endToken ||
(found == 0 || lineText.slice(found - endToken.length, found) == endToken ||
!/comment/.test(cm.getTokenTypeAt(CodeMirror.Pos(line, found))))) {
startCh = found + startToken.length;
break;

View File

@ -49,7 +49,7 @@
});
var myRange = cm.markText(range.from, range.to, {
replacedWith: myWidget,
clearOnEnter: true,
clearOnEnter: getOption(cm, options, "clearOnEnter"),
__isFold: true
});
myRange.on("clear", function(from, to) {
@ -129,7 +129,8 @@
rangeFinder: CodeMirror.fold.auto,
widget: "\u2194",
minFoldSize: 0,
scanUp: false
scanUp: false,
clearOnEnter: true
};
CodeMirror.defineOption("foldOptions", null);

View File

@ -50,7 +50,7 @@
}
function isFolded(cm, line) {
var marks = cm.findMarksAt(Pos(line));
var marks = cm.findMarks(Pos(line, 0), Pos(line + 1, 0));
for (var i = 0; i < marks.length; ++i)
if (marks[i].__isFold && marks[i].find().from.line == line) return marks[i];
}

View File

@ -11,32 +11,36 @@
})(function(CodeMirror) {
"use strict";
function lineIndent(cm, lineNo) {
var text = cm.getLine(lineNo)
var spaceTo = text.search(/\S/)
if (spaceTo == -1 || /\bcomment\b/.test(cm.getTokenTypeAt(CodeMirror.Pos(lineNo, spaceTo + 1))))
return -1
return CodeMirror.countColumn(text, null, cm.getOption("tabSize"))
}
CodeMirror.registerHelper("fold", "indent", function(cm, start) {
var tabSize = cm.getOption("tabSize"), firstLine = cm.getLine(start.line);
if (!/\S/.test(firstLine)) return;
var getIndent = function(line) {
return CodeMirror.countColumn(line, null, tabSize);
};
var myIndent = getIndent(firstLine);
var lastLineInFold = null;
var myIndent = lineIndent(cm, start.line)
if (myIndent < 0) return
var lastLineInFold = null
// Go through lines until we find a line that definitely doesn't belong in
// the block we're folding, or to the end.
for (var i = start.line + 1, end = cm.lastLine(); i <= end; ++i) {
var curLine = cm.getLine(i);
var curIndent = getIndent(curLine);
if (curIndent > myIndent) {
var indent = lineIndent(cm, i)
if (indent == -1) {
} else if (indent > myIndent) {
// Lines with a greater indent are considered part of the block.
lastLineInFold = i;
} else if (!/\S/.test(curLine)) {
// Empty lines might be breaks within the block we're trying to fold.
} else {
// A non-empty line at an indent equal to or less than ours marks the
// start of another block.
// If this line has non-space, non-comment content, and is
// indented less or equal to the start line, it is the start of
// another block.
break;
}
}
if (lastLineInFold) return {
from: CodeMirror.Pos(start.line, firstLine.length),
from: CodeMirror.Pos(start.line, cm.getLine(start.line).length),
to: CodeMirror.Pos(lastLineInFold, cm.getLine(lastLineInFold).length)
};
});

View File

@ -21,8 +21,8 @@
function Iter(cm, line, ch, range) {
this.line = line; this.ch = ch;
this.cm = cm; this.text = cm.getLine(line);
this.min = range ? range.from : cm.firstLine();
this.max = range ? range.to - 1 : cm.lastLine();
this.min = range ? Math.max(range.from, cm.firstLine()) : cm.firstLine();
this.max = range ? Math.min(range.to - 1, cm.lastLine()) : cm.lastLine();
}
function tagAt(iter, ch) {
@ -140,9 +140,9 @@
var openTag = toNextTag(iter), end;
if (!openTag || iter.line != start.line || !(end = toTagEnd(iter))) return;
if (!openTag[1] && end != "selfClose") {
var start = Pos(iter.line, iter.ch);
var close = findMatchingClose(iter, openTag[2]);
return close && {from: start, to: close.from};
var startPos = Pos(iter.line, iter.ch);
var endPos = findMatchingClose(iter, openTag[2]);
return endPos && {from: startPos, to: endPos.from};
}
}
});
@ -163,10 +163,10 @@
}
};
CodeMirror.findEnclosingTag = function(cm, pos, range) {
CodeMirror.findEnclosingTag = function(cm, pos, range, tag) {
var iter = new Iter(cm, pos.line, pos.ch, range);
for (;;) {
var open = findMatchingOpen(iter);
var open = findMatchingOpen(iter, tag);
if (!open) break;
var forward = new Iter(cm, pos.line, pos.ch, range);
var close = findMatchingClose(forward, open.tag);

View File

@ -97,6 +97,15 @@
var coffeescriptKeywords = ("and break catch class continue delete do else extends false finally for " +
"if in instanceof isnt new no not null of off on or return switch then throw true try typeof until void while with yes").split(" ");
function forAllProps(obj, callback) {
if (!Object.getOwnPropertyNames || !Object.getPrototypeOf) {
for (var name in obj) callback(name)
} else {
for (var o = obj; o; o = Object.getPrototypeOf(o))
Object.getOwnPropertyNames(o).forEach(callback)
}
}
function getCompletions(token, context, keywords, options) {
var found = [], start = token.string, global = options && options.globalScope || window;
function maybeAdd(str) {
@ -106,7 +115,7 @@
if (typeof obj == "string") forEach(stringProps, maybeAdd);
else if (obj instanceof Array) forEach(arrayProps, maybeAdd);
else if (obj instanceof Function) forEach(funcProps, maybeAdd);
for (var name in obj) maybeAdd(name);
forAllProps(obj, maybeAdd)
}
if (context && context.length) {

View File

@ -25,8 +25,6 @@
margin: 0;
padding: 0 4px;
border-radius: 2px;
max-width: 19em;
overflow: hidden;
white-space: pre;
color: black;
cursor: pointer;

View File

@ -229,6 +229,9 @@
var winH = window.innerHeight || Math.max(document.body.offsetHeight, document.documentElement.offsetHeight);
(completion.options.container || document.body).appendChild(hints);
var box = hints.getBoundingClientRect(), overlapY = box.bottom - winH;
var scrolls = hints.scrollHeight > hints.clientHeight + 1
var startScroll = cm.getScrollInfo();
if (overlapY > 0) {
var height = box.bottom - box.top, curTop = pos.top - (pos.bottom - box.top);
if (curTop - height > 0) { // Fits above cursor
@ -253,6 +256,8 @@
}
hints.style.left = (left = pos.left - overlapX) + "px";
}
if (scrolls) for (var node = hints.firstChild; node; node = node.nextSibling)
node.style.paddingRight = cm.display.nativeBarWidth + "px"
cm.addKeyMap(this.keyMap = buildKeyMap(completion, {
moveFocus: function(n, avoidWrap) { widget.changeActive(widget.selectedHint + n, avoidWrap); },
@ -270,7 +275,6 @@
cm.on("focus", this.onFocus = function() { clearTimeout(closingOnBlur); });
}
var startScroll = cm.getScrollInfo();
cm.on("scroll", this.onScroll = function() {
var curScroll = cm.getScrollInfo(), editor = cm.getWrapperElement().getBoundingClientRect();
var newTop = top + startScroll.top - curScroll.top;

View File

@ -14,11 +14,12 @@
var tables;
var defaultTable;
var keywords;
var identifierQuote;
var CONS = {
QUERY_DIV: ";",
ALIAS_KEYWORD: "AS"
};
var Pos = CodeMirror.Pos;
var Pos = CodeMirror.Pos, cmpPos = CodeMirror.cmpPos;
function isArray(val) { return Object.prototype.toString.call(val) == "[object Array]" }
@ -28,6 +29,12 @@
return CodeMirror.resolveMode(mode).keywords;
}
function getIdentifierQuote(editor) {
var mode = editor.doc.modeOption;
if (mode === "sql") mode = "text/x-sql";
return CodeMirror.resolveMode(mode).identifierQuote || "`";
}
function getText(item) {
return typeof item == "string" ? item : item.text;
}
@ -86,17 +93,25 @@
}
function cleanName(name) {
// Get rid name from backticks(`) and preceding dot(.)
// Get rid name from identifierQuote and preceding dot(.)
if (name.charAt(0) == ".") {
name = name.substr(1);
}
return name.replace(/`/g, "");
// replace doublicated identifierQuotes with single identifierQuotes
// and remove single identifierQuotes
var nameParts = name.split(identifierQuote+identifierQuote);
for (var i = 0; i < nameParts.length; i++)
nameParts[i] = nameParts[i].replace(new RegExp(identifierQuote,"g"), "");
return nameParts.join(identifierQuote);
}
function insertBackticks(name) {
function insertIdentifierQuotes(name) {
var nameParts = getText(name).split(".");
for (var i = 0; i < nameParts.length; i++)
nameParts[i] = "`" + nameParts[i] + "`";
nameParts[i] = identifierQuote +
// doublicate identifierQuotes
nameParts[i].replace(new RegExp(identifierQuote,"g"), identifierQuote+identifierQuote) +
identifierQuote;
var escaped = nameParts.join(".");
if (typeof name == "string") return escaped;
name = shallowClone(name);
@ -106,13 +121,13 @@
function nameCompletion(cur, token, result, editor) {
// Try to complete table, column names and return start position of completion
var useBacktick = false;
var useIdentifierQuotes = false;
var nameParts = [];
var start = token.start;
var cont = true;
while (cont) {
cont = (token.string.charAt(0) == ".");
useBacktick = useBacktick || (token.string.charAt(0) == "`");
useIdentifierQuotes = useIdentifierQuotes || (token.string.charAt(0) == identifierQuote);
start = token.start;
nameParts.unshift(cleanName(token.string));
@ -127,12 +142,12 @@
// Try to complete table names
var string = nameParts.join(".");
addMatches(result, string, tables, function(w) {
return useBacktick ? insertBackticks(w) : w;
return useIdentifierQuotes ? insertIdentifierQuotes(w) : w;
});
// Try to complete columns from defaultTable
addMatches(result, string, defaultTable, function(w) {
return useBacktick ? insertBackticks(w) : w;
return useIdentifierQuotes ? insertIdentifierQuotes(w) : w;
});
// Try to complete columns
@ -162,7 +177,7 @@
w = shallowClone(w);
w.text = tableInsert + "." + w.text;
}
return useBacktick ? insertBackticks(w) : w;
return useIdentifierQuotes ? insertIdentifierQuotes(w) : w;
});
}
@ -170,21 +185,9 @@
}
function eachWord(lineText, f) {
if (!lineText) return;
var excepted = /[,;]/g;
var words = lineText.split(" ");
for (var i = 0; i < words.length; i++) {
f(words[i]?words[i].replace(excepted, '') : '');
}
}
function convertCurToNumber(cur) {
// max characters of a line is 999,999.
return cur.line + cur.ch / Math.pow(10, 6);
}
function convertNumberToCur(num) {
return Pos(Math.floor(num), +num.toString().split('.').pop());
var words = lineText.split(/\s+/)
for (var i = 0; i < words.length; i++)
if (words[i]) f(words[i].replace(/[,;]/g, ''))
}
function findTableByAlias(alias, editor) {
@ -209,15 +212,14 @@
separator.push(Pos(editor.lastLine(), editor.getLineHandle(editor.lastLine()).text.length));
//find valid range
var prevItem = 0;
var current = convertCurToNumber(editor.getCursor());
var prevItem = null;
var current = editor.getCursor()
for (var i = 0; i < separator.length; i++) {
var _v = convertCurToNumber(separator[i]);
if (current > prevItem && current <= _v) {
validRange = { start: convertNumberToCur(prevItem), end: convertNumberToCur(_v) };
if ((prevItem == null || cmpPos(current, prevItem) > 0) && cmpPos(current, separator[i]) <= 0) {
validRange = {start: prevItem, end: separator[i]};
break;
}
prevItem = _v;
prevItem = separator[i];
}
var query = doc.getRange(validRange.start, validRange.end, false);
@ -241,7 +243,8 @@
var defaultTableName = options && options.defaultTable;
var disableKeywords = options && options.disableKeywords;
defaultTable = defaultTableName && getTable(defaultTableName);
keywords = keywords || getKeywords(editor);
keywords = getKeywords(editor);
identifierQuote = getIdentifierQuote(editor);
if (defaultTableName && !defaultTable)
defaultTable = findTableByAlias(defaultTableName, editor);
@ -259,7 +262,7 @@
token.string = token.string.slice(0, cur.ch - token.start);
}
if (token.string.match(/^[.`\w@]\w*$/)) {
if (token.string.match(/^[.`"\w@]\w*$/)) {
search = token.string;
start = token.start;
end = token.end;
@ -267,7 +270,7 @@
start = end = cur.ch;
search = "";
}
if (search.charAt(0) == "." || search.charAt(0) == "`") {
if (search.charAt(0) == "." || search.charAt(0) == identifierQuote) {
start = nameCompletion(cur, token, result, editor);
} else {
addMatches(result, search, tables, function(w) {return w;});

View File

@ -4,10 +4,10 @@
}
.CodeMirror-lint-tooltip {
background-color: infobackground;
background-color: #ffd;
border: 1px solid black;
border-radius: 4px 4px 4px 4px;
color: infotext;
color: black;
font-family: monospace;
font-size: 10pt;
overflow: hidden;

View File

@ -140,7 +140,12 @@
if (options.async || getAnnotations.async) {
lintAsync(cm, getAnnotations, passOptions)
} else {
updateLinting(cm, getAnnotations(cm.getValue(), passOptions, cm));
var annotations = getAnnotations(cm.getValue(), passOptions, cm);
if (!annotations) return;
if (annotations.then) annotations.then(function(issues) {
updateLinting(cm, issues);
});
else updateLinting(cm, annotations);
}
}
@ -226,7 +231,7 @@
var state = cm.state.lint = new LintState(cm, parseOptions(cm, val), hasLintGutter);
if (state.options.lintOnChange !== false)
cm.on("change", onChange);
if (state.options.tooltips != false)
if (state.options.tooltips != false && state.options.tooltips != "gutter")
CodeMirror.on(cm.getWrapperElement(), "mouseover", state.onMouseOver);
startLinting(cm);

View File

@ -19,8 +19,15 @@ CodeMirror.registerHelper("lint", "yaml", function(text) {
var found = [];
try { jsyaml.load(text); }
catch(e) {
var loc = e.mark;
found.push({ from: CodeMirror.Pos(loc.line, loc.column), to: CodeMirror.Pos(loc.line, loc.column), message: e.message });
var loc = e.mark,
// js-yaml YAMLException doesn't always provide an accurate lineno
// e.g., when there are multiple yaml docs
// ---
// ---
// foo:bar
from = loc ? CodeMirror.Pos(loc.line, loc.column) : CodeMirror.Pos(0, 0),
to = from;
found.push({ from: from, to: to, message: e.message });
}
return found;
});

View File

@ -37,18 +37,29 @@
constructor: DiffView,
init: function(pane, orig, options) {
this.edit = this.mv.edit;
(this.edit.state.diffViews || (this.edit.state.diffViews = [])).push(this);
;(this.edit.state.diffViews || (this.edit.state.diffViews = [])).push(this);
this.orig = CodeMirror(pane, copyObj({value: orig, readOnly: !this.mv.options.allowEditingOriginals}, copyObj(options)));
this.orig.state.diffViews = [this];
if (this.mv.options.connect == "align") {
if (!this.edit.state.trackAlignable) this.edit.state.trackAlignable = new TrackAlignable(this.edit)
this.orig.state.trackAlignable = new TrackAlignable(this.orig)
}
this.diff = getDiff(asString(orig), asString(options.value));
this.orig.state.diffViews = [this];
var classLocation = options.chunkClassLocation || "background";
if (Object.prototype.toString.call(classLocation) != "[object Array]") classLocation = [classLocation]
this.classes.classLocation = classLocation
this.diff = getDiff(asString(orig), asString(options.value), this.mv.options.ignoreWhitespace);
this.chunks = getChunks(this.diff);
this.diffOutOfDate = this.dealigned = false;
this.needsScrollSync = null
this.showDifferences = options.showDifferences !== false;
},
registerEvents: function(otherDv) {
this.forceUpdate = registerUpdate(this);
setScrollLock(this, true, false);
registerScroll(this);
registerScroll(this, otherDv);
},
setShowDifferences: function(val) {
val = val !== false;
@ -61,7 +72,7 @@
function ensureDiff(dv) {
if (dv.diffOutOfDate) {
dv.diff = getDiff(dv.orig.getValue(), dv.edit.getValue());
dv.diff = getDiff(dv.orig.getValue(), dv.edit.getValue(), dv.mv.options.ignoreWhitespace);
dv.chunks = getChunks(dv.diff);
dv.diffOutOfDate = false;
CodeMirror.signal(dv.edit, "updateDiff", dv.diff);
@ -88,10 +99,12 @@
updateMarks(dv.edit, dv.diff, edit, DIFF_INSERT, dv.classes);
updateMarks(dv.orig, dv.diff, orig, DIFF_DELETE, dv.classes);
}
makeConnections(dv);
if (dv.mv.options.connect == "align")
alignChunks(dv);
makeConnections(dv);
if (dv.needsScrollSync != null) syncScroll(dv, dv.needsScrollSync)
updating = false;
}
function setDealign(fast) {
@ -113,37 +126,49 @@
// Update faster when a line was added/removed
setDealign(change.text.length - 1 != change.to.line - change.from.line);
}
function swapDoc() {
dv.diffOutOfDate = true;
dv.dealigned = true;
update("full");
}
dv.edit.on("change", change);
dv.orig.on("change", change);
dv.edit.on("markerAdded", setDealign);
dv.edit.on("markerCleared", setDealign);
dv.orig.on("markerAdded", setDealign);
dv.orig.on("markerCleared", setDealign);
dv.edit.on("swapDoc", swapDoc);
dv.orig.on("swapDoc", swapDoc);
if (dv.mv.options.connect == "align") {
CodeMirror.on(dv.edit.state.trackAlignable, "realign", setDealign)
CodeMirror.on(dv.orig.state.trackAlignable, "realign", setDealign)
}
dv.edit.on("viewportChange", function() { set(false); });
dv.orig.on("viewportChange", function() { set(false); });
update();
return update;
}
function registerScroll(dv) {
function registerScroll(dv, otherDv) {
dv.edit.on("scroll", function() {
syncScroll(dv, DIFF_INSERT) && makeConnections(dv);
syncScroll(dv, true) && makeConnections(dv);
});
dv.orig.on("scroll", function() {
syncScroll(dv, DIFF_DELETE) && makeConnections(dv);
syncScroll(dv, false) && makeConnections(dv);
if (otherDv) syncScroll(otherDv, true) && makeConnections(otherDv);
});
}
function syncScroll(dv, type) {
function syncScroll(dv, toOrig) {
// Change handler will do a refresh after a timeout when diff is out of date
if (dv.diffOutOfDate) return false;
if (dv.diffOutOfDate) {
if (dv.lockScroll && dv.needsScrollSync == null) dv.needsScrollSync = toOrig
return false
}
dv.needsScrollSync = null
if (!dv.lockScroll) return true;
var editor, other, now = +new Date;
if (type == DIFF_INSERT) { editor = dv.edit; other = dv.orig; }
if (toOrig) { editor = dv.edit; other = dv.orig; }
else { editor = dv.orig; other = dv.edit; }
// Don't take action if the position of this editor was recently set
// (to prevent feedback loops)
if (editor.state.scrollSetBy == dv && (editor.state.scrollSetAt || 0) + 50 > now) return false;
if (editor.state.scrollSetBy == dv && (editor.state.scrollSetAt || 0) + 250 > now) return false;
var sInfo = editor.getScrollInfo();
if (dv.mv.options.connect == "align") {
@ -151,9 +176,9 @@
} else {
var halfScreen = .5 * sInfo.clientHeight, midY = sInfo.top + halfScreen;
var mid = editor.lineAtHeight(midY, "local");
var around = chunkBoundariesAround(dv.chunks, mid, type == DIFF_INSERT);
var off = getOffsets(editor, type == DIFF_INSERT ? around.edit : around.orig);
var offOther = getOffsets(other, type == DIFF_INSERT ? around.orig : around.edit);
var around = chunkBoundariesAround(dv.chunks, mid, toOrig);
var off = getOffsets(editor, toOrig ? around.edit : around.orig);
var offOther = getOffsets(other, toOrig ? around.orig : around.edit);
var ratio = (midY - off.top) / (off.bot - off.top);
var targetPos = (offOther.top - halfScreen) + ratio * (offOther.bot - offOther.top);
@ -191,16 +216,22 @@
// Updating the marks for editor content
function removeClass(editor, line, classes) {
var locs = classes.classLocation
for (var i = 0; i < locs.length; i++) {
editor.removeLineClass(line, locs[i], classes.chunk);
editor.removeLineClass(line, locs[i], classes.start);
editor.removeLineClass(line, locs[i], classes.end);
}
}
function clearMarks(editor, arr, classes) {
for (var i = 0; i < arr.length; ++i) {
var mark = arr[i];
if (mark instanceof CodeMirror.TextMarker) {
if (mark instanceof CodeMirror.TextMarker)
mark.clear();
} else if (mark.parent) {
editor.removeLineClass(mark, "background", classes.chunk);
editor.removeLineClass(mark, "background", classes.start);
editor.removeLineClass(mark, "background", classes.end);
}
else if (mark.parent)
removeClass(editor, mark, classes);
}
arr.length = 0;
}
@ -226,28 +257,34 @@
});
}
function addClass(editor, lineNr, classes, main, start, end) {
var locs = classes.classLocation, line = editor.getLineHandle(lineNr);
for (var i = 0; i < locs.length; i++) {
if (main) editor.addLineClass(line, locs[i], classes.chunk);
if (start) editor.addLineClass(line, locs[i], classes.start);
if (end) editor.addLineClass(line, locs[i], classes.end);
}
return line;
}
function markChanges(editor, diff, type, marks, from, to, classes) {
var pos = Pos(0, 0);
var top = Pos(from, 0), bot = editor.clipPos(Pos(to - 1));
var cls = type == DIFF_DELETE ? classes.del : classes.insert;
function markChunk(start, end) {
var bfrom = Math.max(from, start), bto = Math.min(to, end);
for (var i = bfrom; i < bto; ++i) {
var line = editor.addLineClass(i, "background", classes.chunk);
if (i == start) editor.addLineClass(line, "background", classes.start);
if (i == end - 1) editor.addLineClass(line, "background", classes.end);
marks.push(line);
}
for (var i = bfrom; i < bto; ++i)
marks.push(addClass(editor, i, classes, true, i == start, i == end - 1));
// When the chunk is empty, make sure a horizontal line shows up
if (start == end && bfrom == end && bto == end) {
if (bfrom)
marks.push(editor.addLineClass(bfrom - 1, "background", classes.end));
marks.push(addClass(editor, bfrom - 1, classes, false, false, true));
else
marks.push(editor.addLineClass(bfrom, "background", classes.start));
marks.push(addClass(editor, bfrom, classes, false, true, false));
}
}
var chunkStart = 0;
var chunkStart = 0, pending = false;
for (var i = 0; i < diff.length; ++i) {
var part = diff[i], tp = part[0], str = part[1];
if (tp == DIFF_EQUAL) {
@ -255,10 +292,11 @@
moveOver(pos, str);
var cleanTo = pos.line + (endOfLineClean(diff, i) ? 1 : 0);
if (cleanTo > cleanFrom) {
if (i) markChunk(chunkStart, cleanFrom);
if (pending) { markChunk(chunkStart, cleanFrom); pending = false }
chunkStart = cleanTo;
}
} else {
pending = true
if (tp == type) {
var end = moveOver(pos, str, true);
var a = posMax(top, pos), b = posMin(bot, end);
@ -268,7 +306,7 @@
}
}
}
if (chunkStart <= pos.line) markChunk(chunkStart, pos.line + 1);
if (pending) markChunk(chunkStart, pos.line + 1);
}
// Updating the gap between editor and original
@ -284,7 +322,9 @@
if (dv.copyButtons) clear(dv.copyButtons);
var vpEdit = dv.edit.getViewport(), vpOrig = dv.orig.getViewport();
var sTopEdit = dv.edit.getScrollInfo().top, sTopOrig = dv.orig.getScrollInfo().top;
var outerTop = dv.mv.wrap.getBoundingClientRect().top
var sTopEdit = outerTop - dv.edit.getScrollerElement().getBoundingClientRect().top + dv.edit.getScrollInfo().top
var sTopOrig = outerTop - dv.orig.getScrollerElement().getBoundingClientRect().top + dv.orig.getScrollInfo().top;
for (var i = 0; i < dv.chunks.length; i++) {
var ch = dv.chunks[i];
if (ch.editFrom <= vpEdit.to && ch.editTo >= vpEdit.from &&
@ -305,29 +345,81 @@
return origStart + (editLine - editStart);
}
function findAlignedLines(dv, other) {
var linesToAlign = [];
for (var i = 0; i < dv.chunks.length; i++) {
var chunk = dv.chunks[i];
linesToAlign.push([chunk.origTo, chunk.editTo, other ? getMatchingOrigLine(chunk.editTo, other.chunks) : null]);
// Combines information about chunks and widgets/markers to return
// an array of lines, in a single editor, that probably need to be
// aligned with their counterparts in the editor next to it.
function alignableFor(cm, chunks, isOrig) {
var tracker = cm.state.trackAlignable
var start = cm.firstLine(), trackI = 0
var result = []
for (var i = 0;; i++) {
var chunk = chunks[i]
var chunkStart = !chunk ? 1e9 : isOrig ? chunk.origFrom : chunk.editFrom
for (; trackI < tracker.alignable.length; trackI += 2) {
var n = tracker.alignable[trackI] + 1
if (n <= start) continue
if (n <= chunkStart) result.push(n)
else break
}
if (!chunk) break
result.push(start = isOrig ? chunk.origTo : chunk.editTo)
}
if (other) {
for (var i = 0; i < other.chunks.length; i++) {
var chunk = other.chunks[i];
for (var j = 0; j < linesToAlign.length; j++) {
var align = linesToAlign[j];
if (align[1] == chunk.editTo) {
j = -1;
break;
} else if (align[1] > chunk.editTo) {
break;
}
return result
}
// Given information about alignable lines in two editors, fill in
// the result (an array of three-element arrays) to reflect the
// lines that need to be aligned with each other.
function mergeAlignable(result, origAlignable, chunks, setIndex) {
var rI = 0, origI = 0, chunkI = 0, diff = 0
outer: for (;; rI++) {
var nextR = result[rI], nextO = origAlignable[origI]
if (!nextR && nextO == null) break
var rLine = nextR ? nextR[0] : 1e9, oLine = nextO == null ? 1e9 : nextO
while (chunkI < chunks.length) {
var chunk = chunks[chunkI]
if (chunk.origFrom <= oLine && chunk.origTo > oLine) {
origI++
rI--
continue outer;
}
if (j > -1)
linesToAlign.splice(j - 1, 0, [getMatchingOrigLine(chunk.editTo, dv.chunks), chunk.editTo, chunk.origTo]);
if (chunk.editTo > rLine) {
if (chunk.editFrom <= rLine) continue outer;
break
}
diff += (chunk.origTo - chunk.origFrom) - (chunk.editTo - chunk.editFrom)
chunkI++
}
if (rLine == oLine - diff) {
nextR[setIndex] = oLine
origI++
} else if (rLine < oLine - diff) {
nextR[setIndex] = rLine + diff
} else {
var record = [oLine - diff, null, null]
record[setIndex] = oLine
result.splice(rI, 0, record)
origI++
}
}
return linesToAlign;
}
function findAlignedLines(dv, other) {
var alignable = alignableFor(dv.edit, dv.chunks, false), result = []
if (other) for (var i = 0, j = 0; i < other.chunks.length; i++) {
var n = other.chunks[i].editTo
while (j < alignable.length && alignable[j] < n) j++
if (j == alignable.length || alignable[j] != n) alignable.splice(j++, 0, n)
}
for (var i = 0; i < alignable.length; i++)
result.push([alignable[i], null, null])
mergeAlignable(result, alignableFor(dv.orig, dv.chunks, true), dv.chunks, 1)
if (other)
mergeAlignable(result, alignableFor(other.orig, other.chunks, true), other.chunks, 2)
return result
}
function alignChunks(dv, force) {
@ -350,7 +442,7 @@
aligners[i].clear();
aligners.length = 0;
var cm = [dv.orig, dv.edit], scroll = [];
var cm = [dv.edit, dv.orig], scroll = [];
if (other) cm.push(other.orig);
for (var i = 0; i < cm.length; i++)
scroll.push(cm[i].getScrollInfo().top);
@ -385,18 +477,18 @@
var elt = document.createElement("div");
elt.className = "CodeMirror-merge-spacer";
elt.style.height = size + "px"; elt.style.minWidth = "1px";
return cm.addLineWidget(line, elt, {height: size, above: above});
return cm.addLineWidget(line, elt, {height: size, above: above, mergeSpacer: true, handleMouseEvents: true});
}
function drawConnectorsForChunk(dv, chunk, sTopOrig, sTopEdit, w) {
var flip = dv.type == "left";
var top = dv.orig.heightAtLine(chunk.origFrom, "local") - sTopOrig;
var top = dv.orig.heightAtLine(chunk.origFrom, "local", true) - sTopOrig;
if (dv.svg) {
var topLpx = top;
var topRpx = dv.edit.heightAtLine(chunk.editFrom, "local") - sTopEdit;
var topRpx = dv.edit.heightAtLine(chunk.editFrom, "local", true) - sTopEdit;
if (flip) { var tmp = topLpx; topLpx = topRpx; topRpx = tmp; }
var botLpx = dv.orig.heightAtLine(chunk.origTo, "local") - sTopOrig;
var botRpx = dv.edit.heightAtLine(chunk.editTo, "local") - sTopEdit;
var botLpx = dv.orig.heightAtLine(chunk.origTo, "local", true) - sTopOrig;
var botRpx = dv.edit.heightAtLine(chunk.editTo, "local", true) - sTopEdit;
if (flip) { var tmp = botLpx; botLpx = botRpx; botRpx = tmp; }
var curveTop = " C " + w/2 + " " + topRpx + " " + w/2 + " " + topLpx + " " + (w + 2) + " " + topLpx;
var curveBot = " C " + w/2 + " " + botLpx + " " + w/2 + " " + botRpx + " -1 " + botRpx;
@ -410,10 +502,10 @@
var editOriginals = dv.mv.options.allowEditingOriginals;
copy.title = editOriginals ? "Push to left" : "Revert chunk";
copy.chunk = chunk;
copy.style.top = top + "px";
copy.style.top = (chunk.origTo > chunk.origFrom ? top : dv.edit.heightAtLine(chunk.editFrom, "local") - sTopEdit) + "px";
if (editOriginals) {
var topReverse = dv.orig.heightAtLine(chunk.editFrom, "local") - sTopEdit;
var topReverse = dv.edit.heightAtLine(chunk.editFrom, "local") - sTopEdit;
var copyReverse = dv.copyButtons.appendChild(elt("div", dv.type == "right" ? "\u21dd" : "\u21dc",
"CodeMirror-merge-copy-reverse"));
copyReverse.title = "Push to right";
@ -427,9 +519,15 @@
function copyChunk(dv, to, from, chunk) {
if (dv.diffOutOfDate) return;
var editStart = chunk.editTo > to.lastLine() ? Pos(chunk.editFrom - 1) : Pos(chunk.editFrom, 0)
var origStart = chunk.origTo > from.lastLine() ? Pos(chunk.origFrom - 1) : Pos(chunk.origFrom, 0)
to.replaceRange(from.getRange(origStart, Pos(chunk.origTo, 0)), editStart, Pos(chunk.editTo, 0))
var origEnd = Pos(chunk.origTo, 0)
var editStart = chunk.editTo > to.lastLine() ? Pos(chunk.editFrom - 1) : Pos(chunk.editFrom, 0)
var editEnd = Pos(chunk.editTo, 0)
var handler = dv.mv.options.revertChunk
if (handler)
handler(dv.mv, from, origStart, origEnd, to, editStart, editEnd)
else
to.replaceRange(from.getRange(origStart, origEnd), editStart, editEnd)
}
// Merge view, containing 0, 1, or 2 diff views.
@ -447,18 +545,18 @@
if (hasLeft) {
left = this.left = new DiffView(this, "left");
var leftPane = elt("div", null, "CodeMirror-merge-pane");
var leftPane = elt("div", null, "CodeMirror-merge-pane CodeMirror-merge-left");
wrap.push(leftPane);
wrap.push(buildGap(left));
}
var editPane = elt("div", null, "CodeMirror-merge-pane");
var editPane = elt("div", null, "CodeMirror-merge-pane CodeMirror-merge-editor");
wrap.push(editPane);
if (hasRight) {
right = this.right = new DiffView(this, "right");
wrap.push(buildGap(right));
var rightPane = elt("div", null, "CodeMirror-merge-pane");
var rightPane = elt("div", null, "CodeMirror-merge-pane CodeMirror-merge-right");
wrap.push(rightPane);
}
@ -471,7 +569,6 @@
if (left) left.init(leftPane, origLeft, options);
if (right) right.init(rightPane, origRight, options);
if (options.collapseIdentical)
this.editor().operation(function() {
collapseIdenticalStretches(self, options.collapseIdentical);
@ -480,6 +577,9 @@
this.aligners = [];
alignChunks(this.left || this.right, true);
}
if (left) left.registerEvents(right)
if (right) right.registerEvents(left)
var onResize = function() {
if (left) makeConnections(left);
@ -522,7 +622,7 @@
}
MergeView.prototype = {
constuctor: MergeView,
constructor: MergeView,
editor: function() { return this.edit; },
rightOriginal: function() { return this.right && this.right.orig; },
leftOriginal: function() { return this.left && this.left.orig; },
@ -546,13 +646,12 @@
// Operations on diffs
var dmp = new diff_match_patch();
function getDiff(a, b) {
function getDiff(a, b, ignoreWhitespace) {
var diff = dmp.diff_main(a, b);
dmp.diff_cleanupSemantic(diff);
// The library sometimes leaves in empty parts, which confuse the algorithm
for (var i = 0; i < diff.length; ++i) {
var part = diff[i];
if (!part[1]) {
if (ignoreWhitespace ? !/[^ \t]/.test(part[1]) : !part[1]) {
diff.splice(i--, 1);
} else if (i && diff[i - 1][0] == part[0]) {
diff.splice(i--, 1);
@ -569,7 +668,7 @@
for (var i = 0; i < diff.length; ++i) {
var part = diff[i], tp = part[0];
if (tp == DIFF_EQUAL) {
var startOff = startOfLineClean(diff, i) ? 0 : 1;
var startOff = !startOfLineClean(diff, i) || edit.line < startEdit || orig.line < startOrig ? 1 : 0;
var cleanFromEdit = edit.line + startOff, cleanFromOrig = orig.line + startOff;
moveOver(edit, part[1], null, orig);
var endOff = endOfLineClean(diff, i) ? 1 : 0;
@ -592,10 +691,10 @@
function endOfLineClean(diff, i) {
if (i == diff.length - 1) return true;
var next = diff[i + 1][1];
if (next.length == 1 || next.charCodeAt(0) != 10) return false;
if ((next.length == 1 && i < diff.length - 2) || next.charCodeAt(0) != 10) return false;
if (i == diff.length - 2) return true;
next = diff[i + 2][1];
return next.length > 1 && next.charCodeAt(0) == 10;
return (next.length > 1 || i == diff.length - 3) && next.charCodeAt(0) == 10;
}
function startOfLineClean(diff, i) {
@ -729,6 +828,131 @@
return out;
}
// Tracks collapsed markers and line widgets, in order to be able to
// accurately align the content of two editors.
var F_WIDGET = 1, F_WIDGET_BELOW = 2, F_MARKER = 4
function TrackAlignable(cm) {
this.cm = cm
this.alignable = []
this.height = cm.doc.height
var self = this
cm.on("markerAdded", function(_, marker) {
if (!marker.collapsed) return
var found = marker.find(1)
if (found != null) self.set(found.line, F_MARKER)
})
cm.on("markerCleared", function(_, marker, _min, max) {
if (max != null && marker.collapsed)
self.check(max, F_MARKER, self.hasMarker)
})
cm.on("markerChanged", this.signal.bind(this))
cm.on("lineWidgetAdded", function(_, widget, lineNo) {
if (widget.mergeSpacer) return
if (widget.above) self.set(lineNo - 1, F_WIDGET_BELOW)
else self.set(lineNo, F_WIDGET)
})
cm.on("lineWidgetCleared", function(_, widget, lineNo) {
if (widget.mergeSpacer) return
if (widget.above) self.check(lineNo - 1, F_WIDGET_BELOW, self.hasWidgetBelow)
else self.check(lineNo, F_WIDGET, self.hasWidget)
})
cm.on("lineWidgetChanged", this.signal.bind(this))
cm.on("change", function(_, change) {
var start = change.from.line, nBefore = change.to.line - change.from.line
var nAfter = change.text.length - 1, end = start + nAfter
if (nBefore || nAfter) self.map(start, nBefore, nAfter)
self.check(end, F_MARKER, self.hasMarker)
if (nBefore || nAfter) self.check(change.from.line, F_MARKER, self.hasMarker)
})
cm.on("viewportChange", function() {
if (self.cm.doc.height != self.height) self.signal()
})
}
TrackAlignable.prototype = {
signal: function() {
CodeMirror.signal(this, "realign")
this.height = this.cm.doc.height
},
set: function(n, flags) {
var pos = -1
for (; pos < this.alignable.length; pos += 2) {
var diff = this.alignable[pos] - n
if (diff == 0) {
if ((this.alignable[pos + 1] & flags) == flags) return
this.alignable[pos + 1] |= flags
this.signal()
return
}
if (diff > 0) break
}
this.signal()
this.alignable.splice(pos, 0, n, flags)
},
find: function(n) {
for (var i = 0; i < this.alignable.length; i += 2)
if (this.alignable[i] == n) return i
return -1
},
check: function(n, flag, pred) {
var found = this.find(n)
if (found == -1 || !(this.alignable[found + 1] & flag)) return
if (!pred.call(this, n)) {
this.signal()
var flags = this.alignable[found + 1] & ~flag
if (flags) this.alignable[found + 1] = flags
else this.alignable.splice(found, 2)
}
},
hasMarker: function(n) {
var handle = this.cm.getLineHandle(n)
if (handle.markedSpans) for (var i = 0; i < handle.markedSpans.length; i++)
if (handle.markedSpans[i].mark.collapsed && handle.markedSpans[i].to != null)
return true
return false
},
hasWidget: function(n) {
var handle = this.cm.getLineHandle(n)
if (handle.widgets) for (var i = 0; i < handle.widgets.length; i++)
if (!handle.widgets[i].above && !handle.widgets[i].mergeSpacer) return true
return false
},
hasWidgetBelow: function(n) {
if (n == this.cm.lastLine()) return false
var handle = this.cm.getLineHandle(n + 1)
if (handle.widgets) for (var i = 0; i < handle.widgets.length; i++)
if (handle.widgets[i].above && !handle.widgets[i].mergeSpacer) return true
return false
},
map: function(from, nBefore, nAfter) {
var diff = nAfter - nBefore, to = from + nBefore, widgetFrom = -1, widgetTo = -1
for (var i = 0; i < this.alignable.length; i += 2) {
var n = this.alignable[i]
if (n == from && (this.alignable[i + 1] & F_WIDGET_BELOW)) widgetFrom = i
if (n == to && (this.alignable[i + 1] & F_WIDGET_BELOW)) widgetTo = i
if (n <= from) continue
else if (n < to) this.alignable.splice(i--, 2)
else this.alignable[i] += diff
}
if (widgetFrom > -1) {
var flags = this.alignable[widgetFrom + 1]
if (flags == F_WIDGET_BELOW) this.alignable.splice(widgetFrom, 2)
else this.alignable[widgetFrom + 1] = flags & ~F_WIDGET_BELOW
}
if (widgetTo > -1 && nAfter)
this.set(from + nAfter, F_WIDGET_BELOW)
}
}
function posMin(a, b) { return (a.line - b.line || a.ch - b.ch) < 0 ? a : b; }
function posMax(a, b) { return (a.line - b.line || a.ch - b.ch) > 0 ? a : b; }
function posEq(a, b) { return a.line == b.line && a.ch == b.ch; }

View File

@ -76,8 +76,13 @@ CodeMirror.overlayMode = function(base, overlay, combine) {
innerMode: function(state) { return {state: state.base, mode: base}; },
blankLine: function(state) {
if (base.blankLine) base.blankLine(state.base);
if (overlay.blankLine) overlay.blankLine(state.overlay);
var baseToken, overlayToken;
if (base.blankLine) baseToken = base.blankLine(state.base);
if (overlay.blankLine) overlayToken = overlay.blankLine(state.overlay);
return overlayToken == null ?
baseToken :
(combine && baseToken != null ? baseToken + " " + overlayToken : overlayToken);
}
};
};

View File

@ -77,17 +77,21 @@
curLine = pos.line;
curLineObj = cm.getLineHandle(curLine);
}
if (wrapping && curLineObj.height > singleLineH)
if ((curLineObj.widgets && curLineObj.widgets.length) ||
(wrapping && curLineObj.height > singleLineH))
return cm.charCoords(pos, "local")[top ? "top" : "bottom"];
var topY = cm.heightAtLine(curLineObj, "local");
return topY + (top ? 0 : curLineObj.height);
}
var lastLine = cm.lastLine()
if (cm.display.barWidth) for (var i = 0, nextTop; i < anns.length; i++) {
var ann = anns[i];
if (ann.to.line > lastLine) continue;
var top = nextTop || getY(ann.from, true) * hScale;
var bottom = getY(ann.to, false) * hScale;
while (i < anns.length - 1) {
if (anns[i + 1].to.line > lastLine) break;
nextTop = getY(anns[i + 1].from, true) * hScale;
if (nextTop > bottom + .9) break;
ann = anns[++i];

View File

@ -40,7 +40,9 @@
if (cm.state.scrollPastEndPadding != padding) {
cm.state.scrollPastEndPadding = padding;
cm.display.lineSpace.parentNode.style.paddingBottom = padding;
cm.off("refresh", updateBottomMargin);
cm.setSize();
cm.on("refresh", updateBottomMargin);
}
}
});

View File

@ -59,10 +59,10 @@
CodeMirror.on(this.node, "DOMMouseScroll", onWheel);
}
Bar.prototype.setPos = function(pos) {
Bar.prototype.setPos = function(pos, force) {
if (pos < 0) pos = 0;
if (pos > this.total - this.screen) pos = this.total - this.screen;
if (pos == this.pos) return false;
if (!force && pos == this.pos) return false;
this.pos = pos;
this.inner.style[this.orientation == "horizontal" ? "left" : "top"] =
(pos * (this.size / this.total)) + "px";
@ -76,9 +76,12 @@
var minButtonSize = 10;
Bar.prototype.update = function(scrollSize, clientSize, barSize) {
this.screen = clientSize;
this.total = scrollSize;
this.size = barSize;
var sizeChanged = this.screen != clientSize || this.total != scrollSize || this.size != barSize
if (sizeChanged) {
this.screen = clientSize;
this.total = scrollSize;
this.size = barSize;
}
var buttonSize = this.screen * (this.size / this.total);
if (buttonSize < minButtonSize) {
@ -87,7 +90,7 @@
}
this.inner.style[this.orientation == "horizontal" ? "width" : "height"] =
buttonSize + "px";
this.setPos(this.pos);
this.setPos(this.pos, sizeChanged);
};
function SimpleScrollbars(cls, place, scroll) {

View File

@ -45,6 +45,7 @@
this.options[name] = (options && options.hasOwnProperty(name) ? options : defaults)[name]
this.overlay = this.timeout = null;
this.matchesonscroll = null;
this.active = false;
}
CodeMirror.defineOption("highlightSelectionMatches", false, function(cm, val, old) {
@ -53,16 +54,34 @@
clearTimeout(cm.state.matchHighlighter.timeout);
cm.state.matchHighlighter = null;
cm.off("cursorActivity", cursorActivity);
cm.off("focus", onFocus)
}
if (val) {
cm.state.matchHighlighter = new State(val);
highlightMatches(cm);
var state = cm.state.matchHighlighter = new State(val);
if (cm.hasFocus()) {
state.active = true
highlightMatches(cm)
} else {
cm.on("focus", onFocus)
}
cm.on("cursorActivity", cursorActivity);
}
});
function cursorActivity(cm) {
var state = cm.state.matchHighlighter;
if (state.active || cm.hasFocus()) scheduleHighlight(cm, state)
}
function onFocus(cm) {
var state = cm.state.matchHighlighter
if (!state.active) {
state.active = true
scheduleHighlight(cm, state)
}
}
function scheduleHighlight(cm, state) {
clearTimeout(state.timeout);
state.timeout = setTimeout(function() {highlightMatches(cm);}, state.options.delay);
}
@ -72,7 +91,7 @@
cm.addOverlay(state.overlay = makeOverlay(query, hasBoundary, style));
if (state.options.annotateScrollbar && cm.showMatchesOnScrollbar) {
var searchFor = hasBoundary ? new RegExp("\\b" + query + "\\b") : query;
state.matchesonscroll = cm.showMatchesOnScrollbar(searchFor, true,
state.matchesonscroll = cm.showMatchesOnScrollbar(searchFor, false,
{className: "CodeMirror-selection-highlight-scrollbar"});
}
}

View File

@ -54,15 +54,16 @@
function getSearchCursor(cm, query, pos) {
// Heuristic: if the query string is all lowercase, do a case insensitive search.
return cm.getSearchCursor(query, pos, queryCaseInsensitive(query));
return cm.getSearchCursor(query, pos, {caseFold: queryCaseInsensitive(query), multiline: true});
}
function persistentDialog(cm, text, deflt, f) {
cm.openDialog(text, f, {
function persistentDialog(cm, text, deflt, onEnter, onKeyDown) {
cm.openDialog(text, onEnter, {
value: deflt,
selectValueOnOpen: true,
closeOnEnter: false,
onClose: function() { clearSearch(cm); }
onClose: function() { clearSearch(cm); },
onKeyDown: onKeyDown
});
}
@ -98,7 +99,7 @@
}
var queryDialog =
'Search: <input type="text" style="width: 10em" class="CodeMirror-search-field"/> <span style="color: #888" class="CodeMirror-search-hint">(Use /re/ syntax for regexp search)</span>';
'<span class="CodeMirror-search-label">Search:</span> <input type="text" style="width: 10em" class="CodeMirror-search-field"/> <span style="color: #888" class="CodeMirror-search-hint">(Use /re/ syntax for regexp search)</span>';
function startSearch(cm, state, query) {
state.queryText = query;
@ -112,13 +113,13 @@
}
}
function doSearch(cm, rev, persistent) {
function doSearch(cm, rev, persistent, immediate) {
var state = getSearchState(cm);
if (state.query) return findNext(cm, rev);
var q = cm.getSelection() || state.lastQuery;
if (persistent && cm.openDialog) {
var hiding = null
persistentDialog(cm, queryDialog, q, function(query, event) {
var searchNext = function(query, event) {
CodeMirror.e_stop(event);
if (!query) return;
if (query != state.queryText) {
@ -133,7 +134,25 @@
dialog.getBoundingClientRect().bottom - 4 > cm.cursorCoords(to, "window").top)
(hiding = dialog).style.opacity = .4
})
};
persistentDialog(cm, queryDialog, q, searchNext, function(event, query) {
var keyName = CodeMirror.keyName(event)
var cmd = CodeMirror.keyMap[cm.getOption("keyMap")][keyName]
if (!cmd) cmd = cm.getOption('extraKeys')[keyName]
if (cmd == "findNext" || cmd == "findPrev" ||
cmd == "findPersistentNext" || cmd == "findPersistentPrev") {
CodeMirror.e_stop(event);
startSearch(cm, getSearchState(cm), query);
cm.execCommand(cmd);
} else if (cmd == "find" || cmd == "findPersistent") {
CodeMirror.e_stop(event);
searchNext(query, event);
}
});
if (immediate && q) {
startSearch(cm, state, q);
findNext(cm, rev);
}
} else {
dialog(cm, queryDialog, "Search for:", q, function(query) {
if (query && !state.query) cm.operation(function() {
@ -169,8 +188,8 @@
var replaceQueryDialog =
' <input type="text" style="width: 10em" class="CodeMirror-search-field"/> <span style="color: #888" class="CodeMirror-search-hint">(Use /re/ syntax for regexp search)</span>';
var replacementQueryDialog = 'With: <input type="text" style="width: 10em" class="CodeMirror-search-field"/>';
var doReplaceConfirm = "Replace? <button>Yes</button> <button>No</button> <button>All</button> <button>Stop</button>";
var replacementQueryDialog = '<span class="CodeMirror-search-label">With:</span> <input type="text" style="width: 10em" class="CodeMirror-search-field"/>';
var doReplaceConfirm = '<span class="CodeMirror-search-label">Replace?</span> <button>Yes</button> <button>No</button> <button>All</button> <button>Stop</button>';
function replaceAll(cm, query, text) {
cm.operation(function() {
@ -186,7 +205,7 @@
function replace(cm, all) {
if (cm.getOption("readOnly")) return;
var query = cm.getSelection() || getSearchState(cm).lastQuery;
var dialogText = all ? "Replace all:" : "Replace:"
var dialogText = '<span class="CodeMirror-search-label">' + (all ? 'Replace all:' : 'Replace:') + '</span>';
dialog(cm, dialogText + replaceQueryDialog, dialogText, query, function(query) {
if (!query) return;
query = parseQuery(query);
@ -223,6 +242,8 @@
CodeMirror.commands.find = function(cm) {clearSearch(cm); doSearch(cm);};
CodeMirror.commands.findPersistent = function(cm) {clearSearch(cm); doSearch(cm, false, true);};
CodeMirror.commands.findPersistentNext = function(cm) {doSearch(cm, false, true, true);};
CodeMirror.commands.findPersistentPrev = function(cm) {doSearch(cm, true, true, true);};
CodeMirror.commands.findNext = doSearch;
CodeMirror.commands.findPrev = function(cm) {doSearch(cm, true);};
CodeMirror.commands.clearSearch = clearSearch;

View File

@ -3,187 +3,279 @@
(function(mod) {
if (typeof exports == "object" && typeof module == "object") // CommonJS
mod(require("../../lib/codemirror"));
mod(require("../../lib/codemirror"))
else if (typeof define == "function" && define.amd) // AMD
define(["../../lib/codemirror"], mod);
define(["../../lib/codemirror"], mod)
else // Plain browser env
mod(CodeMirror);
mod(CodeMirror)
})(function(CodeMirror) {
"use strict";
var Pos = CodeMirror.Pos;
"use strict"
var Pos = CodeMirror.Pos
function SearchCursor(doc, query, pos, caseFold) {
this.atOccurrence = false; this.doc = doc;
if (caseFold == null && typeof query == "string") caseFold = false;
function regexpFlags(regexp) {
var flags = regexp.flags
return flags != null ? flags : (regexp.ignoreCase ? "i" : "")
+ (regexp.global ? "g" : "")
+ (regexp.multiline ? "m" : "")
}
pos = pos ? doc.clipPos(pos) : Pos(0, 0);
this.pos = {from: pos, to: pos};
function ensureGlobal(regexp) {
return regexp.global ? regexp : new RegExp(regexp.source, regexpFlags(regexp) + "g")
}
// The matches method is filled in based on the type of query.
// It takes a position and a direction, and returns an object
// describing the next occurrence of the query, or null if no
// more matches were found.
if (typeof query != "string") { // Regexp match
if (!query.global) query = new RegExp(query.source, query.ignoreCase ? "ig" : "g");
this.matches = function(reverse, pos) {
if (reverse) {
query.lastIndex = 0;
var line = doc.getLine(pos.line).slice(0, pos.ch), cutOff = 0, match, start;
for (;;) {
query.lastIndex = cutOff;
var newMatch = query.exec(line);
if (!newMatch) break;
match = newMatch;
start = match.index;
cutOff = match.index + (match[0].length || 1);
if (cutOff == line.length) break;
}
var matchLen = (match && match[0].length) || 0;
if (!matchLen) {
if (start == 0 && line.length == 0) {match = undefined;}
else if (start != doc.getLine(pos.line).length) {
matchLen++;
}
}
} else {
query.lastIndex = pos.ch;
var line = doc.getLine(pos.line), match = query.exec(line);
var matchLen = (match && match[0].length) || 0;
var start = match && match.index;
if (start + matchLen != line.length && !matchLen) matchLen = 1;
}
if (match && matchLen)
return {from: Pos(pos.line, start),
to: Pos(pos.line, start + matchLen),
match: match};
};
} else { // String query
var origQuery = query;
if (caseFold) query = query.toLowerCase();
var fold = caseFold ? function(str){return str.toLowerCase();} : function(str){return str;};
var target = query.split("\n");
// Different methods for single-line and multi-line queries
if (target.length == 1) {
if (!query.length) {
// Empty string would match anything and never progress, so
// we define it to match nothing instead.
this.matches = function() {};
} else {
this.matches = function(reverse, pos) {
if (reverse) {
var orig = doc.getLine(pos.line).slice(0, pos.ch), line = fold(orig);
var match = line.lastIndexOf(query);
if (match > -1) {
match = adjustPos(orig, line, match);
return {from: Pos(pos.line, match), to: Pos(pos.line, match + origQuery.length)};
}
} else {
var orig = doc.getLine(pos.line).slice(pos.ch), line = fold(orig);
var match = line.indexOf(query);
if (match > -1) {
match = adjustPos(orig, line, match) + pos.ch;
return {from: Pos(pos.line, match), to: Pos(pos.line, match + origQuery.length)};
}
}
};
}
} else {
var origTarget = origQuery.split("\n");
this.matches = function(reverse, pos) {
var last = target.length - 1;
if (reverse) {
if (pos.line - (target.length - 1) < doc.firstLine()) return;
if (fold(doc.getLine(pos.line).slice(0, origTarget[last].length)) != target[target.length - 1]) return;
var to = Pos(pos.line, origTarget[last].length);
for (var ln = pos.line - 1, i = last - 1; i >= 1; --i, --ln)
if (target[i] != fold(doc.getLine(ln))) return;
var line = doc.getLine(ln), cut = line.length - origTarget[0].length;
if (fold(line.slice(cut)) != target[0]) return;
return {from: Pos(ln, cut), to: to};
} else {
if (pos.line + (target.length - 1) > doc.lastLine()) return;
var line = doc.getLine(pos.line), cut = line.length - origTarget[0].length;
if (fold(line.slice(cut)) != target[0]) return;
var from = Pos(pos.line, cut);
for (var ln = pos.line + 1, i = 1; i < last; ++i, ++ln)
if (target[i] != fold(doc.getLine(ln))) return;
if (fold(doc.getLine(ln).slice(0, origTarget[last].length)) != target[last]) return;
return {from: from, to: Pos(ln, origTarget[last].length)};
}
};
function maybeMultiline(regexp) {
return /\\s|\\n|\n|\\W|\\D|\[\^/.test(regexp.source)
}
function searchRegexpForward(doc, regexp, start) {
regexp = ensureGlobal(regexp)
for (var line = start.line, ch = start.ch, last = doc.lastLine(); line <= last; line++, ch = 0) {
regexp.lastIndex = ch
var string = doc.getLine(line), match = regexp.exec(string)
if (match)
return {from: Pos(line, match.index),
to: Pos(line, match.index + match[0].length),
match: match}
}
}
function searchRegexpForwardMultiline(doc, regexp, start) {
if (!maybeMultiline(regexp)) return searchRegexpForward(doc, regexp, start)
regexp = ensureGlobal(regexp)
var string, chunk = 1
for (var line = start.line, last = doc.lastLine(); line <= last;) {
// This grows the search buffer in exponentially-sized chunks
// between matches, so that nearby matches are fast and don't
// require concatenating the whole document (in case we're
// searching for something that has tons of matches), but at the
// same time, the amount of retries is limited.
for (var i = 0; i < chunk; i++) {
var curLine = doc.getLine(line++)
string = string == null ? curLine : string + "\n" + curLine
}
chunk = chunk * 2
regexp.lastIndex = start.ch
var match = regexp.exec(string)
if (match) {
var before = string.slice(0, match.index).split("\n"), inside = match[0].split("\n")
var startLine = start.line + before.length - 1, startCh = before[before.length - 1].length
return {from: Pos(startLine, startCh),
to: Pos(startLine + inside.length - 1,
inside.length == 1 ? startCh + inside[0].length : inside[inside.length - 1].length),
match: match}
}
}
}
SearchCursor.prototype = {
findNext: function() {return this.find(false);},
findPrevious: function() {return this.find(true);},
find: function(reverse) {
var self = this, pos = this.doc.clipPos(reverse ? this.pos.from : this.pos.to);
function savePosAndFail(line) {
var pos = Pos(line, 0);
self.pos = {from: pos, to: pos};
self.atOccurrence = false;
return false;
}
for (;;) {
if (this.pos = this.matches(reverse, pos)) {
this.atOccurrence = true;
return this.pos.match || true;
}
if (reverse) {
if (!pos.line) return savePosAndFail(0);
pos = Pos(pos.line-1, this.doc.getLine(pos.line-1).length);
}
else {
var maxLine = this.doc.lineCount();
if (pos.line == maxLine - 1) return savePosAndFail(maxLine);
pos = Pos(pos.line + 1, 0);
}
}
},
from: function() {if (this.atOccurrence) return this.pos.from;},
to: function() {if (this.atOccurrence) return this.pos.to;},
replace: function(newText, origin) {
if (!this.atOccurrence) return;
var lines = CodeMirror.splitLines(newText);
this.doc.replaceRange(lines, this.pos.from, this.pos.to, origin);
this.pos.to = Pos(this.pos.from.line + lines.length - 1,
lines[lines.length - 1].length + (lines.length == 1 ? this.pos.from.ch : 0));
function lastMatchIn(string, regexp) {
var cutOff = 0, match
for (;;) {
regexp.lastIndex = cutOff
var newMatch = regexp.exec(string)
if (!newMatch) return match
match = newMatch
cutOff = match.index + (match[0].length || 1)
if (cutOff == string.length) return match
}
};
}
function searchRegexpBackward(doc, regexp, start) {
regexp = ensureGlobal(regexp)
for (var line = start.line, ch = start.ch, first = doc.firstLine(); line >= first; line--, ch = -1) {
var string = doc.getLine(line)
if (ch > -1) string = string.slice(0, ch)
var match = lastMatchIn(string, regexp)
if (match)
return {from: Pos(line, match.index),
to: Pos(line, match.index + match[0].length),
match: match}
}
}
function searchRegexpBackwardMultiline(doc, regexp, start) {
regexp = ensureGlobal(regexp)
var string, chunk = 1
for (var line = start.line, first = doc.firstLine(); line >= first;) {
for (var i = 0; i < chunk; i++) {
var curLine = doc.getLine(line--)
string = string == null ? curLine.slice(0, start.ch) : curLine + "\n" + string
}
chunk *= 2
var match = lastMatchIn(string, regexp)
if (match) {
var before = string.slice(0, match.index).split("\n"), inside = match[0].split("\n")
var startLine = line + before.length, startCh = before[before.length - 1].length
return {from: Pos(startLine, startCh),
to: Pos(startLine + inside.length - 1,
inside.length == 1 ? startCh + inside[0].length : inside[inside.length - 1].length),
match: match}
}
}
}
function doFold(str) { return str.toLowerCase() }
function noFold(str) { return str }
// Maps a position in a case-folded line back to a position in the original line
// (compensating for codepoints increasing in number during folding)
function adjustPos(orig, folded, pos) {
if (orig.length == folded.length) return pos;
if (orig.length == folded.length) return pos
for (var pos1 = Math.min(pos, orig.length);;) {
var len1 = orig.slice(0, pos1).toLowerCase().length;
if (len1 < pos) ++pos1;
else if (len1 > pos) --pos1;
else return pos1;
var len1 = orig.slice(0, pos1).toLowerCase().length
if (len1 < pos) ++pos1
else if (len1 > pos) --pos1
else return pos1
}
}
function searchStringForward(doc, query, start, caseFold) {
// Empty string would match anything and never progress, so we
// define it to match nothing instead.
if (!query.length) return null
var fold = caseFold ? doFold : noFold
var lines = fold(query).split(/\r|\n\r?/)
search: for (var line = start.line, ch = start.ch, last = doc.lastLine() + 1 - lines.length; line <= last; line++, ch = 0) {
var orig = doc.getLine(line).slice(ch), string = fold(orig)
if (lines.length == 1) {
var found = string.indexOf(lines[0])
if (found == -1) continue search
var start = adjustPos(orig, string, found) + ch
return {from: Pos(line, adjustPos(orig, string, found) + ch),
to: Pos(line, adjustPos(orig, string, found + lines[0].length) + ch)}
} else {
var cutFrom = string.length - lines[0].length
if (string.slice(cutFrom) != lines[0]) continue search
for (var i = 1; i < lines.length - 1; i++)
if (fold(doc.getLine(line + i)) != lines[i]) continue search
var end = doc.getLine(line + lines.length - 1), endString = fold(end), lastLine = lines[lines.length - 1]
if (end.slice(0, lastLine.length) != lastLine) continue search
return {from: Pos(line, adjustPos(orig, string, cutFrom) + ch),
to: Pos(line + lines.length - 1, adjustPos(end, endString, lastLine.length))}
}
}
}
function searchStringBackward(doc, query, start, caseFold) {
if (!query.length) return null
var fold = caseFold ? doFold : noFold
var lines = fold(query).split(/\r|\n\r?/)
search: for (var line = start.line, ch = start.ch, first = doc.firstLine() - 1 + lines.length; line >= first; line--, ch = -1) {
var orig = doc.getLine(line)
if (ch > -1) orig = orig.slice(0, ch)
var string = fold(orig)
if (lines.length == 1) {
var found = string.lastIndexOf(lines[0])
if (found == -1) continue search
return {from: Pos(line, adjustPos(orig, string, found)),
to: Pos(line, adjustPos(orig, string, found + lines[0].length))}
} else {
var lastLine = lines[lines.length - 1]
if (string.slice(0, lastLine.length) != lastLine) continue search
for (var i = 1, start = line - lines.length + 1; i < lines.length - 1; i++)
if (fold(doc.getLine(start + i)) != lines[i]) continue search
var top = doc.getLine(line + 1 - lines.length), topString = fold(top)
if (topString.slice(topString.length - lines[0].length) != lines[0]) continue search
return {from: Pos(line + 1 - lines.length, adjustPos(top, topString, top.length - lines[0].length)),
to: Pos(line, adjustPos(orig, string, lastLine.length))}
}
}
}
function SearchCursor(doc, query, pos, options) {
this.atOccurrence = false
this.doc = doc
pos = pos ? doc.clipPos(pos) : Pos(0, 0)
this.pos = {from: pos, to: pos}
var caseFold
if (typeof options == "object") {
caseFold = options.caseFold
} else { // Backwards compat for when caseFold was the 4th argument
caseFold = options
options = null
}
if (typeof query == "string") {
if (caseFold == null) caseFold = false
this.matches = function(reverse, pos) {
return (reverse ? searchStringBackward : searchStringForward)(doc, query, pos, caseFold)
}
} else {
query = ensureGlobal(query)
if (!options || options.multiline !== false)
this.matches = function(reverse, pos) {
return (reverse ? searchRegexpBackwardMultiline : searchRegexpForwardMultiline)(doc, query, pos)
}
else
this.matches = function(reverse, pos) {
return (reverse ? searchRegexpBackward : searchRegexpForward)(doc, query, pos)
}
}
}
SearchCursor.prototype = {
findNext: function() {return this.find(false)},
findPrevious: function() {return this.find(true)},
find: function(reverse) {
var result = this.matches(reverse, this.doc.clipPos(reverse ? this.pos.from : this.pos.to))
// Implements weird auto-growing behavior on null-matches for
// backwards-compatiblity with the vim code (unfortunately)
while (result && CodeMirror.cmpPos(result.from, result.to) == 0) {
if (reverse) {
if (result.from.ch) result.from = Pos(result.from.line, result.from.ch - 1)
else if (result.from.line == this.doc.firstLine()) result = null
else result = this.matches(reverse, this.doc.clipPos(Pos(result.from.line - 1)))
} else {
if (result.to.ch < this.doc.getLine(result.to.line).length) result.to = Pos(result.to.line, result.to.ch + 1)
else if (result.to.line == this.doc.lastLine()) result = null
else result = this.matches(reverse, Pos(result.to.line + 1, 0))
}
}
if (result) {
this.pos = result
this.atOccurrence = true
return this.pos.match || true
} else {
var end = Pos(reverse ? this.doc.firstLine() : this.doc.lastLine() + 1, 0)
this.pos = {from: end, to: end}
return this.atOccurrence = false
}
},
from: function() {if (this.atOccurrence) return this.pos.from},
to: function() {if (this.atOccurrence) return this.pos.to},
replace: function(newText, origin) {
if (!this.atOccurrence) return
var lines = CodeMirror.splitLines(newText)
this.doc.replaceRange(lines, this.pos.from, this.pos.to, origin)
this.pos.to = Pos(this.pos.from.line + lines.length - 1,
lines[lines.length - 1].length + (lines.length == 1 ? this.pos.from.ch : 0))
}
}
CodeMirror.defineExtension("getSearchCursor", function(query, pos, caseFold) {
return new SearchCursor(this.doc, query, pos, caseFold);
});
return new SearchCursor(this.doc, query, pos, caseFold)
})
CodeMirror.defineDocExtension("getSearchCursor", function(query, pos, caseFold) {
return new SearchCursor(this, query, pos, caseFold);
});
return new SearchCursor(this, query, pos, caseFold)
})
CodeMirror.defineExtension("selectMatches", function(query, caseFold) {
var ranges = [];
var cur = this.getSearchCursor(query, this.getCursor("from"), caseFold);
var ranges = []
var cur = this.getSearchCursor(query, this.getCursor("from"), caseFold)
while (cur.findNext()) {
if (CodeMirror.cmpPos(cur.to(), this.getCursor("to")) > 0) break;
ranges.push({anchor: cur.from(), head: cur.to()});
if (CodeMirror.cmpPos(cur.to(), this.getCursor("to")) > 0) break
ranges.push({anchor: cur.from(), head: cur.to()})
}
if (ranges.length)
this.setSelections(ranges, 0);
});
this.setSelections(ranges, 0)
})
});

View File

@ -1,12 +1,6 @@
// CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: http://codemirror.net/LICENSE
// Because sometimes you need to style the cursor's line.
//
// Adds an option 'styleActiveLine' which, when enabled, gives the
// active line's wrapping <div> the CSS class "CodeMirror-activeline",
// and gives its background <div> the class "CodeMirror-activeline-background".
(function(mod) {
if (typeof exports == "object" && typeof module == "object") // CommonJS
mod(require("../../lib/codemirror"));
@ -21,16 +15,18 @@
var GUTT_CLASS = "CodeMirror-activeline-gutter";
CodeMirror.defineOption("styleActiveLine", false, function(cm, val, old) {
var prev = old && old != CodeMirror.Init;
if (val && !prev) {
cm.state.activeLines = [];
updateActiveLines(cm, cm.listSelections());
cm.on("beforeSelectionChange", selectionChange);
} else if (!val && prev) {
var prev = old == CodeMirror.Init ? false : old;
if (val == prev) return
if (prev) {
cm.off("beforeSelectionChange", selectionChange);
clearActiveLines(cm);
delete cm.state.activeLines;
}
if (val) {
cm.state.activeLines = [];
updateActiveLines(cm, cm.listSelections());
cm.on("beforeSelectionChange", selectionChange);
}
});
function clearActiveLines(cm) {
@ -52,7 +48,9 @@
var active = [];
for (var i = 0; i < ranges.length; i++) {
var range = ranges[i];
if (!range.empty()) continue;
var option = cm.getOption("styleActiveLine");
if (typeof option == "object" && option.nonEmpty ? range.anchor.line != range.head.line : !range.empty())
continue
var line = cm.getLineHandleVisualStart(range.head.line);
if (active[active.length - 1] != line) active.push(line);
}

View File

@ -34,11 +34,12 @@
});
function onCursorActivity(cm) {
cm.operation(function() { update(cm); });
if (cm.state.markedSelection)
cm.operation(function() { update(cm); });
}
function onChange(cm) {
if (cm.state.markedSelection.length)
if (cm.state.markedSelection && cm.state.markedSelection.length)
cm.operation(function() { clear(cm); });
}

View File

@ -30,7 +30,9 @@
}
function findBreakPoint(text, column, wrapOn, killTrailingSpace) {
for (var at = column; at > 0; --at)
var at = column
while (at < text.length && text.charAt(at) == " ") at++
for (; at > 0; --at)
if (wrapOn.test(text.slice(at - 1, at + 1))) break;
for (var first = true;; first = false) {
var endOfText = at;

View File

@ -52,7 +52,7 @@
}
.cm-fat-cursor .CodeMirror-cursor {
width: auto;
border: 0;
border: 0 !important;
background: #7e7;
}
.cm-fat-cursor div.CodeMirror-cursors {
@ -88,8 +88,14 @@
.cm-tab { display: inline-block; text-decoration: inherit; }
.CodeMirror-rulers {
position: absolute;
left: 0; right: 0; top: -50px; bottom: -20px;
overflow: hidden;
}
.CodeMirror-ruler {
border-left: 1px solid #ccc;
top: 0; bottom: 0;
position: absolute;
}
@ -113,7 +119,7 @@
.cm-s-default .cm-property,
.cm-s-default .cm-operator {}
.cm-s-default .cm-variable-2 {color: #05a;}
.cm-s-default .cm-variable-3 {color: #085;}
.cm-s-default .cm-variable-3, .cm-s-default .cm-type {color: #085;}
.cm-s-default .cm-comment {color: #a50;}
.cm-s-default .cm-string {color: #a11;}
.cm-s-default .cm-string-2 {color: #f50;}
@ -200,9 +206,6 @@ div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;}
display: inline-block;
vertical-align: top;
margin-bottom: -30px;
/* Hack to make IE7 behave */
*zoom:1;
*display:inline;
}
.CodeMirror-gutter-wrapper {
position: absolute;
@ -220,11 +223,8 @@ div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;}
cursor: default;
z-index: 4;
}
.CodeMirror-gutter-wrapper {
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
}
.CodeMirror-gutter-wrapper ::selection { background-color: transparent }
.CodeMirror-gutter-wrapper ::-moz-selection { background-color: transparent }
.CodeMirror-lines {
cursor: text;
@ -246,8 +246,8 @@ div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;}
position: relative;
overflow: visible;
-webkit-tap-highlight-color: transparent;
-webkit-font-variant-ligatures: none;
font-variant-ligatures: none;
-webkit-font-variant-ligatures: contextual;
font-variant-ligatures: contextual;
}
.CodeMirror-wrap pre {
word-wrap: break-word;
@ -269,6 +269,8 @@ div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;}
.CodeMirror-widget {}
.CodeMirror-rtl pre { direction: rtl; }
.CodeMirror-code {
outline: none;
}
@ -291,7 +293,10 @@ div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;}
visibility: hidden;
}
.CodeMirror-cursor { position: absolute; }
.CodeMirror-cursor {
position: absolute;
pointer-events: none;
}
.CodeMirror-measure pre { position: static; }
div.CodeMirror-cursors {
@ -318,9 +323,6 @@ div.CodeMirror-dragcursors {
background: rgba(255, 255, 0, .4);
}
/* IE7 hack to prevent it from returning funny offsetTops on the spans */
.CodeMirror span { *vertical-align: text-bottom; }
/* Used to force a border model for a node */
.cm-force-border { padding-right: .1px; }

File diff suppressed because it is too large Load Diff

View File

@ -32,13 +32,13 @@ CodeMirror.defineMode("sql", function(config, parserConfig) {
if (result !== false) return result;
}
if (support.hexNumber == true &&
if (support.hexNumber &&
((ch == "0" && stream.match(/^[xX][0-9a-fA-F]+/))
|| (ch == "x" || ch == "X") && stream.match(/^'[0-9a-fA-F]+'/))) {
// hex
// ref: http://dev.mysql.com/doc/refman/5.5/en/hexadecimal-literals.html
return "number";
} else if (support.binaryNumber == true &&
} else if (support.binaryNumber &&
(((ch == "b" || ch == "B") && stream.match(/^'[01]+'/))
|| (ch == "0" && stream.match(/^b[01]+/)))) {
// bitstring
@ -48,7 +48,7 @@ CodeMirror.defineMode("sql", function(config, parserConfig) {
// numbers
// ref: http://dev.mysql.com/doc/refman/5.5/en/number-literals.html
stream.match(/^[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?/);
support.decimallessFloat == true && stream.eat('.');
support.decimallessFloat && stream.eat('.');
return "number";
} else if (ch == "?" && (stream.eatSpace() || stream.eol() || stream.eat(";"))) {
// placeholders
@ -58,8 +58,8 @@ CodeMirror.defineMode("sql", function(config, parserConfig) {
// ref: http://dev.mysql.com/doc/refman/5.5/en/string-literals.html
state.tokenize = tokenLiteral(ch);
return state.tokenize(stream, state);
} else if ((((support.nCharCast == true && (ch == "n" || ch == "N"))
|| (support.charsetCast == true && ch == "_" && stream.match(/[a-z][a-z0-9]*/i)))
} else if ((((support.nCharCast && (ch == "n" || ch == "N"))
|| (support.charsetCast && ch == "_" && stream.match(/[a-z][a-z0-9]*/i)))
&& (stream.peek() == "'" || stream.peek() == '"'))) {
// charset casting: _utf8'str', N'str', n'str'
// ref: http://dev.mysql.com/doc/refman/5.5/en/string-literals.html
@ -80,16 +80,16 @@ CodeMirror.defineMode("sql", function(config, parserConfig) {
} else if (ch == "/" && stream.eat("*")) {
// multi-line comments
// ref: https://kb.askmonty.org/en/comment-syntax/
state.tokenize = tokenComment;
state.tokenize = tokenComment(1);
return state.tokenize(stream, state);
} else if (ch == ".") {
// .1 for 0.1
if (support.zerolessFloat == true && stream.match(/^(?:\d+(?:e[+-]?\d+)?)/i)) {
if (support.zerolessFloat && stream.match(/^(?:\d+(?:e[+-]?\d+)?)/i)) {
return "number";
}
// .table_name (ODBC)
// // ref: http://dev.mysql.com/doc/refman/5.6/en/identifier-qualifiers.html
if (support.ODBCdotTable == true && stream.match(/^[a-zA-Z_]+/)) {
if (support.ODBCdotTable && stream.match(/^[a-zA-Z_]+/)) {
return "variable-2";
}
} else if (operatorChars.test(ch)) {
@ -130,20 +130,15 @@ CodeMirror.defineMode("sql", function(config, parserConfig) {
return "string";
};
}
function tokenComment(stream, state) {
while (true) {
if (stream.skipTo("*")) {
stream.next();
if (stream.eat("/")) {
state.tokenize = tokenBase;
break;
}
} else {
stream.skipToEnd();
break;
}
function tokenComment(depth) {
return function(stream, state) {
var m = stream.match(/^.*?(\/\*|\*\/)/)
if (!m) stream.skipToEnd()
else if (m[1] == "/*") state.tokenize = tokenComment(depth + 1)
else if (depth > 1) state.tokenize = tokenComment(depth - 1)
else state.tokenize = tokenBase
return "comment"
}
return "comment";
}
function pushContext(stream, state, type) {
@ -170,7 +165,7 @@ CodeMirror.defineMode("sql", function(config, parserConfig) {
if (state.context && state.context.align == null)
state.context.align = false;
}
if (stream.eatSpace()) return null;
if (state.tokenize == tokenBase && stream.eatSpace()) return null;
var style = state.tokenize(stream, state);
if (style == "comment") return style;
@ -217,6 +212,19 @@ CodeMirror.defineMode("sql", function(config, parserConfig) {
return stream.eatWhile(/\w/) ? "variable-2" : null;
}
// "identifier"
function hookIdentifierDoublequote(stream) {
// Standard SQL /SQLite identifiers
// ref: http://web.archive.org/web/20160813185132/http://savage.net.au/SQL/sql-99.bnf.html#delimited%20identifier
// ref: http://sqlite.org/lang_keywords.html
var ch;
while ((ch = stream.next()) != null) {
if (ch == "\"" && !stream.eat("\"")) return "variable-2";
}
stream.backUp(stream.current().length - 1);
return stream.eatWhile(/\w/) ? "variable-2" : null;
}
// variable token
function hookVar(stream) {
// variables
@ -280,7 +288,7 @@ CodeMirror.defineMode("sql", function(config, parserConfig) {
CodeMirror.defineMIME("text/x-mssql", {
name: "sql",
client: set("charset clear connect edit ego exit go help nopager notee nowarning pager print prompt quit rehash source status system tee"),
keywords: set(sqlKeywords + "begin trigger proc view index for add constraint key primary foreign collate clustered nonclustered declare"),
keywords: set(sqlKeywords + "begin trigger proc view index for add constraint key primary foreign collate clustered nonclustered declare exec"),
builtin: set("bigint numeric bit smallint decimal smallmoney int tinyint money float real char varchar text nchar nvarchar ntext binary varbinary image cursor timestamp hierarchyid uniqueidentifier sql_variant xml table "),
atoms: set("false true null unknown"),
operatorChars: /^[*+\-%<>!=]/,
@ -322,6 +330,36 @@ CodeMirror.defineMode("sql", function(config, parserConfig) {
}
});
// provided by the phpLiteAdmin project - phpliteadmin.org
CodeMirror.defineMIME("text/x-sqlite", {
name: "sql",
// commands of the official SQLite client, ref: https://www.sqlite.org/cli.html#dotcmd
client: set("auth backup bail binary changes check clone databases dbinfo dump echo eqp exit explain fullschema headers help import imposter indexes iotrace limit lint load log mode nullvalue once open output print prompt quit read restore save scanstats schema separator session shell show stats system tables testcase timeout timer trace vfsinfo vfslist vfsname width"),
// ref: http://sqlite.org/lang_keywords.html
keywords: set(sqlKeywords + "abort action add after all analyze attach autoincrement before begin cascade case cast check collate column commit conflict constraint cross current_date current_time current_timestamp database default deferrable deferred detach each else end escape except exclusive exists explain fail for foreign full glob if ignore immediate index indexed initially inner instead intersect isnull key left limit match natural no notnull null of offset outer plan pragma primary query raise recursive references regexp reindex release rename replace restrict right rollback row savepoint temp temporary then to transaction trigger unique using vacuum view virtual when with without"),
// SQLite is weakly typed, ref: http://sqlite.org/datatype3.html. This is just a list of some common types.
builtin: set("bool boolean bit blob decimal double float long longblob longtext medium mediumblob mediumint mediumtext time timestamp tinyblob tinyint tinytext text clob bigint int int2 int8 integer float double char varchar date datetime year unsigned signed numeric real"),
// ref: http://sqlite.org/syntax/literal-value.html
atoms: set("null current_date current_time current_timestamp"),
// ref: http://sqlite.org/lang_expr.html#binaryops
operatorChars: /^[*+\-%<>!=&|/~]/,
// SQLite is weakly typed, ref: http://sqlite.org/datatype3.html. This is just a list of some common types.
dateSQL: set("date time timestamp datetime"),
support: set("decimallessFloat zerolessFloat"),
identifierQuote: "\"", //ref: http://sqlite.org/lang_keywords.html
hooks: {
// bind-parameters ref:http://sqlite.org/lang_expr.html#varparam
"@": hookVar,
":": hookVar,
"?": hookVar,
"$": hookVar,
// The preferred way to escape Identifiers is using double quotes, ref: http://sqlite.org/lang_keywords.html
"\"": hookIdentifierDoublequote,
// there is also support for backtics, ref: http://sqlite.org/lang_keywords.html
"`": hookIdentifier
}
});
// the query language used by Apache Cassandra is called CQL, but this mime type
// is called Cassandra to avoid confusion with Contextual Query Language
CodeMirror.defineMIME("text/x-cassandra", {
@ -366,9 +404,9 @@ CodeMirror.defineMode("sql", function(config, parserConfig) {
// http://www.postgresql.org/docs/9.5/static/datatype.html
builtin: set("bigint int8 bigserial serial8 bit varying varbit boolean bool box bytea character char varchar cidr circle date double precision float8 inet integer int int4 interval json jsonb line lseg macaddr money numeric decimal path pg_lsn point polygon real float4 smallint int2 smallserial serial2 serial serial4 text time without zone with timetz timestamp timestamptz tsquery tsvector txid_snapshot uuid xml"),
atoms: set("false true null unknown"),
operatorChars: /^[*+\-%<>!=&|^]/,
operatorChars: /^[*+\-%<>!=&|^\/#@?~]/,
dateSQL: set("date time timestamp"),
support: set("ODBCdotTable decimallessFloat zerolessFloat binaryNumber hexNumber nCharCast charsetCast commentHash commentSpaceRequired")
support: set("ODBCdotTable decimallessFloat zerolessFloat binaryNumber hexNumber nCharCast charsetCast")
});
// Google's SQL-like query language, GQL

View File

@ -59,6 +59,7 @@
<link type="text/css" rel="stylesheet" href="{{ url_for('static', filename='css/codemirror.overrides.css') }}"/>
<link type="text/css" rel="stylesheet" href="{{ url_for('static', filename='css/pgadmin.css') }}"/>
<link type="text/css" rel="stylesheet" href="{{ url_for('static', filename='css/pgadmin.style.css') }}"/>
<link type="text/css" rel="stylesheet" href="{{ url_for('static', filename='css/codemirror.overrides.css') }}"/>
{% block css_link %}{% endblock %}
<!-- Base template scripts -->

View File

@ -973,14 +973,9 @@ define([
render_history_grid: function() {
var self = this;
// Remove any existing grid first
if (self.history_grid) {
self.history_grid.remove();
}
self.history_collection = new HistoryBundle.HistoryCollection([]);
self.history_collection = new HistoryBundle.historyCollection([]);
let queryHistoryElement = reactComponents.React.createElement(
var queryHistoryElement = reactComponents.React.createElement(
reactComponents.QueryHistory, {historyCollection: self.history_collection});
reactComponents.render(queryHistoryElement, $('#history_grid')[0]);
},
@ -1771,6 +1766,7 @@ define([
},
error: function(e) {
// Enable/Disable query tool button only if is_query_tool is true.
self.resetQueryHistoryObject(self);
self.trigger('pgadmin-sqleditor:loading-icon:hide');
if (self.is_query_tool) {
self.disable_tool_buttons(false);
@ -1856,7 +1852,6 @@ define([
// Show message in message and history tab in case of query tool
self.total_time = self.get_query_run_time(self.query_start_time, self.query_end_time);
self.update_msg_history(true, "", false);
var msg1 = S(gettext("Total query runtime: %s.")).sprintf(self.total_time).value();
var msg2 = S(gettext("%s rows affected.")).sprintf(self.rows_affected).value();
@ -1867,6 +1862,7 @@ define([
var _msg = msg1 + '\n' + msg2;
self.update_msg_history(true, _msg, false);
// If there is additional messages from server then add it to message
if(!_.isNull(data.additional_messages) &&
!_.isUndefined(data.additional_messages)) {
@ -2030,6 +2026,10 @@ define([
}
},
resetQueryHistoryObject: function (history) {
history.total_time = '-';
},
// This function is used to raise appropriate message.
update_msg_history: function(status, msg, clear_grid) {
var self = this;

View File

@ -41,7 +41,6 @@ class AppStarter:
)
self.driver.set_window_size(1024, 1024)
time.sleep(10)
self.driver.get(
"http://" + self.app_config.DEFAULT_SERVER + ":" +
random_server_port)

View File

@ -27,7 +27,7 @@ class PgadminPage:
def __init__(self, driver, app_config):
self.driver = driver
self.app_config = app_config
self.timeout = 20
self.timeout = 30
self.app_start_timeout = 60
def reset_layout(self):

View File

@ -0,0 +1,68 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2017, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import React from 'react';
import $ from '../../pgadmin/static/vendor/jquery/jquery-1.11.2';
import CodeMirror from '../../pgadmin/static/jsx/history/detail/code_mirror';
import jasmineEnzyme from 'jasmine-enzyme';
import {shallow} from 'enzyme';
describe('CodeMirror', () => {
beforeEach(() => {
jasmineEnzyme();
});
describe('#hydrateWhenBecomesVisible', () => {
let codeMirror, isVisibleSpy;
beforeEach(() => {
codeMirror = shallow(<CodeMirror />).instance();
isVisibleSpy = spyOn($.fn, 'is');
spyOn(codeMirror, 'hydrate');
});
describe('when component is visible', () => {
beforeEach(() => {
isVisibleSpy.and.returnValue(true);
});
it('should hydrate the codemirror element', () => {
codeMirror.hydrateWhenBecomesVisible();
expect(codeMirror.hydrate).toHaveBeenCalledTimes(1);
});
});
describe('when component is not visible', () => {
beforeEach(() => {
isVisibleSpy.and.returnValue(false);
});
it('should not hydrate the codemirror element', () => {
codeMirror.hydrateWhenBecomesVisible();
expect(codeMirror.hydrate).not.toHaveBeenCalled();
});
describe('when becomes visible', () => {
beforeEach(() => {
isVisibleSpy.and.returnValue(true);
});
it('should hydrate the codemirror element', (done) => {
setTimeout(() => {
codeMirror.hydrateWhenBecomesVisible();
expect(codeMirror.hydrate).toHaveBeenCalledTimes(1);
done();
}, 150);
});
});
});
});
});

View File

@ -66,10 +66,6 @@ describe('historyCollection', function () {
});
});
describe('sort', function () {
it('doesn\'t sort');
});
describe('when instantiated', function () {
describe('from a history model', function () {
it('has the historyModel', () => {

View File

@ -1,50 +0,0 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2017, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import React from 'react';
import QueryHistoryEntry from '../../../pgadmin/static/jsx/history/query_history_entry';
import {mount} from 'enzyme';
import jasmineEnzyme from 'jasmine-enzyme';
describe('QueryHistoryEntry', () => {
let historyWrapper;
beforeEach(() => {
jasmineEnzyme();
});
describe('for a failed query', () => {
beforeEach(() => {
const historyEntry = {
query: 'second sql statement',
start_time: new Date(2016, 11, 11, 1, 33, 5, 99),
status: false,
};
historyWrapper = mount(<QueryHistoryEntry historyEntry={historyEntry}/>);
});
it('displays a pink background color', () => {
expect(historyWrapper.find('div').first()).toHaveStyle('backgroundColor', '#F7D0D5');
});
});
describe('for a successful query', () => {
beforeEach(() => {
const historyEntry = {
query: 'second sql statement',
start_time: new Date(2016, 11, 11, 1, 33, 5, 99),
status: true,
};
historyWrapper = mount(<QueryHistoryEntry historyEntry={historyEntry}/>);
});
it('does not display a pink background color', () => {
expect(historyWrapper.find('div').first()).toHaveStyle('backgroundColor', '#FFF');
});
});
});

View File

@ -10,30 +10,45 @@
import React from 'react';
import QueryHistory from '../../../pgadmin/static/jsx/history/query_history';
import QueryHistoryEntry from '../../../pgadmin/static/jsx/history/query_history_entry';
import QueryHistoryDetail from '../../../pgadmin/static/jsx/history/query_history_detail';
import HistoryCollection from '../../../pgadmin/static/js/history/history_collection';
import jasmineEnzyme from 'jasmine-enzyme';
import {mount, shallow} from 'enzyme';
import {mount} from 'enzyme';
describe('QueryHistory', () => {
let historyWrapper;
beforeEach(() => {
jasmineEnzyme();
const historyCollection = new HistoryCollection([]);
historyWrapper = shallow(<QueryHistory historyCollection={historyCollection}/>);
});
describe('on construction', () => {
describe('on construction, when there is no history', () => {
beforeEach(function () {
const historyCollection = new HistoryCollection([]);
historyWrapper = mount(<QueryHistory historyCollection={historyCollection}/>);
});
it('has no entries', (done) => {
let foundChildren = historyWrapper.find(QueryHistoryEntry);
expect(foundChildren.length).toBe(0);
done();
});
it('nothing is displayed on right panel', (done) => {
let foundChildren = historyWrapper.find(QueryHistoryDetail);
expect(foundChildren.length).toBe(1);
done();
});
it('does not error', () => {
});
});
describe('when it has history', () => {
describe('when two SQL queries were executed', () => {
let foundChildren;
let queryDetail;
let historyCollection;
beforeEach(() => {
const historyObjects = [
@ -43,59 +58,114 @@ describe('QueryHistory', () => {
status: false,
row_affected: 1,
total_time: '234 msec',
message: 'some other message',
message: 'message from second sql query',
},
{
query: 'first sql statement',
start_time: new Date(2017, 5, 3, 14, 3, 15, 150),
status: true,
row_affected: 2,
row_affected: 12345,
total_time: '14 msec',
message: 'a very important message',
message: 'message from first sql query',
},
];
const historyCollection = new HistoryCollection(historyObjects);
historyCollection = new HistoryCollection(historyObjects);
historyWrapper = mount(<QueryHistory historyCollection={historyCollection}/>);
foundChildren = historyWrapper.find(QueryHistoryEntry);
queryDetail = historyWrapper.find(QueryHistoryDetail);
});
it('has two query history entries', () => {
expect(foundChildren.length).toBe(2);
});
it('displays the SQL of the queries in order', () => {
expect(foundChildren.at(0).text()).toContain('first sql statement');
expect(foundChildren.at(1).text()).toContain('second sql statement');
});
it('displays the formatted timestamp of the queries in chronological order by most recent first', () => {
expect(foundChildren.at(0).text()).toContain('Jun 3 2017 14:03:15');
expect(foundChildren.at(1).text()).toContain('Dec 11 2016 01:33:05');
});
it('displays the number of rows affected', () => {
expect(foundChildren.at(1).text()).toContain('1 rows affected');
expect(foundChildren.at(0).text()).toContain('2 rows affected');
});
it('displays the total time', () => {
expect(foundChildren.at(0).text()).toContain('total time: 14 msec');
expect(foundChildren.at(1).text()).toContain('total time: 234 msec');
});
it('displays the truncated message', () => {
expect(foundChildren.at(0).text()).toContain('a very important message');
expect(foundChildren.at(1).text()).toContain('some other message');
});
describe('when there are one failing and one successful query each', () => {
it('adds a white background color for the successful query', () => {
expect(foundChildren.at(0).find('div').first()).toHaveStyle('backgroundColor', '#FFF');
describe('the main pane', () => {
it('has two query history entries', () => {
expect(foundChildren.length).toBe(2);
});
it('adds a red background color for the failed query', () => {
expect(foundChildren.at(1).find('div').first()).toHaveStyle('backgroundColor', '#F7D0D5');
it('displays the query history entries in order', () => {
expect(foundChildren.at(0).text()).toContain('first sql statement');
expect(foundChildren.at(1).text()).toContain('second sql statement');
});
it('displays the formatted timestamp of the queries in chronological order by most recent first', () => {
expect(foundChildren.at(0).text()).toContain('Jun 3 2017 14:03:15');
expect(foundChildren.at(1).text()).toContain('Dec 11 2016 01:33:05');
});
it('renders the most recent query as selected', () => {
expect(foundChildren.at(0).nodes.length).toBe(1);
expect(foundChildren.at(0).find('QueryHistorySelectedEntry').length).toBe(1);
});
it('renders the older query as not selected', () => {
expect(foundChildren.at(1).nodes.length).toBe(1);
expect(foundChildren.at(1).find('QueryHistoryErrorEntry').length).toBe(1);
});
});
describe('the details pane', () => {
it('displays the formatted timestamp', () => {
expect(queryDetail.at(0).text()).toContain('6-3-17 14:03:15Date');
});
it('displays the number of rows affected', () => {
if (/PhantomJS/.test(window.navigator.userAgent)) {
expect(queryDetail.at(0).text()).toContain('12345Rows Affected');
} else {
expect(queryDetail.at(0).text()).toContain('12,345Rows Affected');
}
});
it('displays the total time', () => {
expect(queryDetail.at(0).text()).toContain('14 msecDuration');
});
it('displays the full message', () => {
expect(queryDetail.at(0).text()).toContain('message from first sql query');
});
it('displays first query SQL', (done) => {
setTimeout(() => {
expect(queryDetail.at(0).text()).toContain('first sql statement');
done();
}, 1000);
});
});
describe('when the older query is clicked on', () => {
beforeEach(() => {
foundChildren.at(1).simulate('click');
});
it('displays the query in the right pane', () => {
expect(queryDetail.at(0).text()).toContain('second sql statement');
});
it('renders the most recent query as selected in the left pane', () => {
expect(foundChildren.at(0).nodes.length).toBe(1);
expect(foundChildren.at(0).find('QueryHistoryVanillaEntry').length).toBe(1);
});
it('renders the older query as selected in the left pane', () => {
expect(foundChildren.at(1).nodes.length).toBe(1);
expect(foundChildren.at(1).find('QueryHistorySelectedErrorEntry').length).toBe(1);
});
});
describe('when a third SQL query is executed', () => {
beforeEach(() => {
historyCollection.add({
query: 'third sql statement',
start_time: new Date(2017, 11, 11, 1, 33, 5, 99),
status: false,
row_affected: 5,
total_time: '26 msec',
message: 'third sql message',
});
});
it('displays third query SQL in the right pane', () => {
expect(queryDetail.at(0).text()).toContain('third sql statement');
});
});
});

View File

@ -828,8 +828,8 @@ bluebird@^3.3.0:
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.0.tgz#791420d7f551eea2897453a8a77653f96606d67c"
bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0:
version "4.11.6"
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.6.tgz#53344adb14617a13f6e8dd2ce28905d1c0ba3215"
version "4.11.7"
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.7.tgz#ddb048e50d9482790094c13eb3fcfc833ce7ab46"
body-parser@^1.16.1:
version "1.17.2"
@ -856,6 +856,10 @@ boom@2.x.x:
dependencies:
hoek "2.x.x"
bowser@^1.6.0:
version "1.7.0"
resolved "https://registry.yarnpkg.com/bowser/-/bowser-1.7.0.tgz#169de4018711f994242bff9a8009e77a1f35e003"
brace-expansion@^1.1.7:
version "1.1.8"
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.8.tgz#c07b211c7c952ec1f8efd51a77ef0d1d3990a292"
@ -1400,6 +1404,12 @@ crypto-browserify@^3.0.0, crypto-browserify@^3.11.0:
public-encrypt "^4.0.0"
randombytes "^2.0.0"
css-in-js-utils@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/css-in-js-utils/-/css-in-js-utils-1.0.3.tgz#9ac7e02f763cf85d94017666565ed68a5b5f3215"
dependencies:
hyphenate-style-name "^1.0.2"
css-color-names@0.0.4:
version "0.0.4"
resolved "https://registry.yarnpkg.com/css-color-names/-/css-color-names-0.0.4.tgz#808adc2e79cf84738069b646cb20ec27beb629e0"
@ -2489,6 +2499,10 @@ https-browserify@0.0.1, https-browserify@~0.0.0:
version "0.0.1"
resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-0.0.1.tgz#3f91365cabe60b77ed0ebba24b454e3e09d95a82"
hyphenate-style-name@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/hyphenate-style-name/-/hyphenate-style-name-1.0.2.tgz#31160a36930adaf1fc04c6074f7eb41465d4ec4b"
iconv-lite@0.4.15, iconv-lite@~0.4.13:
version "0.4.15"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.15.tgz#fe265a218ac6a57cfe854927e9d04c19825eddeb"
@ -2561,6 +2575,13 @@ inline-source-map@~0.6.0:
dependencies:
source-map "~0.5.3"
inline-style-prefixer@^3.0.2:
version "3.0.6"
resolved "https://registry.yarnpkg.com/inline-style-prefixer/-/inline-style-prefixer-3.0.6.tgz#b27fe309b4168a31eaf38c8e8c60ab9e7c11731f"
dependencies:
bowser "^1.6.0"
css-in-js-utils "^1.0.3"
inquirer@^0.12.0:
version "0.12.0"
resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-0.12.0.tgz#1ef2bfd63504df0bc75785fff8c2c41df12f077e"
@ -4043,7 +4064,7 @@ promise@^7.1.1:
dependencies:
asap "~2.0.3"
prop-types@^15.5.4:
prop-types@^15.5.4, prop-types@^15.5.8:
version "15.5.10"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.5.10.tgz#2797dfc3126182e3a95e3dfbb2e893ddd7456154"
dependencies:
@ -4155,6 +4176,20 @@ react-addons-test-utils@~15.4.2:
loose-envify "^1.1.0"
object-assign "^4.1.0"
react-split-pane@^0.1.63:
version "0.1.63"
resolved "https://registry.yarnpkg.com/react-split-pane/-/react-split-pane-0.1.63.tgz#fadb3960cc659911dd05ffbc88acee4be9f53583"
dependencies:
inline-style-prefixer "^3.0.2"
prop-types "^15.5.8"
react-style-proptype "^3.0.0"
react-style-proptype@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/react-style-proptype/-/react-style-proptype-3.0.0.tgz#89e0b646f266c656abb0f0dd8202dbd5036c31e6"
dependencies:
prop-types "^15.5.4"
"react@file:../web/pgadmin/static/vendor/react":
version "15.4.2"
dependencies:
@ -4184,8 +4219,8 @@ read-pkg@^1.0.0:
path-type "^1.0.0"
readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.4, readable-stream@^2.1.5, readable-stream@^2.2.6:
version "2.3.0"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.0.tgz#640f5dcda88c91a8dc60787145629170813a1ed2"
version "2.3.1"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.1.tgz#84e26965bb9e785535ed256e8d38e92c69f09d10"
dependencies:
core-util-is "~1.0.0"
inherits "~2.0.3"