Fossil SCM
Merged in latest trunk. test-markdown-render now accepts files and string literals.
Commit
5618cceb7bf9e8c14108d9fd16c7b8f74cbb8994015e945bed8210e34723cf26
Parent
2e6464284183edc…
22 files changed
+4
-1
+4
-5
+1
-1
+16
-3
+360
-106
+4
+17
-8
+33
-7
+76
-1
+2
+1
-1
+9
-3
+1
+1
+233
-77
+257
-137
+1
-1
+131
-52
+14
-5
+14
-5
+22
-26
+25
~
skins/darkmode/css.txt
~
skins/xekri/css.txt
~
src/builtin.c
~
src/chat.c
~
src/chat.js
~
src/clone.c
~
src/default.css
~
src/fossil.diff.js
~
src/fossil.dom.js
~
src/fossil.popupwidget.js
~
src/fossil.storage.js
~
src/info.c
~
src/markdown.c
~
src/markdown.c
~
src/shell.c
~
src/sqlite3.c
~
src/sqlite3.h
~
src/style.chat.css
~
src/wikiformat.c
~
src/wikiformat.c
~
src/xfer.c
~
www/chat.md
+4
-1
| --- skins/darkmode/css.txt | ||
| +++ skins/darkmode/css.txt | ||
| @@ -520,15 +520,18 @@ | ||
| 520 | 520 | button, |
| 521 | 521 | input, |
| 522 | 522 | optgroup, |
| 523 | 523 | select, |
| 524 | 524 | textarea { |
| 525 | - background-color: inherit; | |
| 525 | + background: inherit; | |
| 526 | 526 | color: inherit; |
| 527 | 527 | font: inherit; |
| 528 | 528 | margin: 0 |
| 529 | 529 | } |
| 530 | +button { | |
| 531 | + background-color: rgba(45,45,45,0.75); | |
| 532 | +} | |
| 530 | 533 | input, textarea, select { |
| 531 | 534 | border: 1px solid rgba(127, 201, 255, 0.9); |
| 532 | 535 | padding: 1px; |
| 533 | 536 | } |
| 534 | 537 | select { |
| 535 | 538 |
| --- skins/darkmode/css.txt | |
| +++ skins/darkmode/css.txt | |
| @@ -520,15 +520,18 @@ | |
| 520 | button, |
| 521 | input, |
| 522 | optgroup, |
| 523 | select, |
| 524 | textarea { |
| 525 | background-color: inherit; |
| 526 | color: inherit; |
| 527 | font: inherit; |
| 528 | margin: 0 |
| 529 | } |
| 530 | input, textarea, select { |
| 531 | border: 1px solid rgba(127, 201, 255, 0.9); |
| 532 | padding: 1px; |
| 533 | } |
| 534 | select { |
| 535 |
| --- skins/darkmode/css.txt | |
| +++ skins/darkmode/css.txt | |
| @@ -520,15 +520,18 @@ | |
| 520 | button, |
| 521 | input, |
| 522 | optgroup, |
| 523 | select, |
| 524 | textarea { |
| 525 | background: inherit; |
| 526 | color: inherit; |
| 527 | font: inherit; |
| 528 | margin: 0 |
| 529 | } |
| 530 | button { |
| 531 | background-color: rgba(45,45,45,0.75); |
| 532 | } |
| 533 | input, textarea, select { |
| 534 | border: 1px solid rgba(127, 201, 255, 0.9); |
| 535 | padding: 1px; |
| 536 | } |
| 537 | select { |
| 538 |
+4
-5
| --- skins/xekri/css.txt | ||
| +++ skins/xekri/css.txt | ||
| @@ -237,11 +237,11 @@ | ||
| 237 | 237 | div.footer div { |
| 238 | 238 | background-color: #222; |
| 239 | 239 | box-shadow: 3px 3px 1px #000; |
| 240 | 240 | border-radius: 0 0 1rem 1rem; |
| 241 | 241 | margin: 0 0 10px 0; |
| 242 | - padding: 0.5rem 0.75rem; | |
| 242 | + padding: 0.25rem 0.75rem; | |
| 243 | 243 | } |
| 244 | 244 | |
| 245 | 245 | div.footer div.page-time { |
| 246 | 246 | float: left; |
| 247 | 247 | } |
| @@ -1157,14 +1157,13 @@ | ||
| 1157 | 1157 | body.chat div.header, body.chat div.footer, |
| 1158 | 1158 | body.chat div.mainmenu, body.chat div.submenu, |
| 1159 | 1159 | body.chat div.content { |
| 1160 | 1160 | margin-left: auto; |
| 1161 | 1161 | margin-right: auto; |
| 1162 | + margin-top: auto/*eliminates unnecessary scrollbars*/; | |
| 1162 | 1163 | } |
| 1163 | 1164 | body.chat.chat-only-mode div.content { |
| 1164 | 1165 | max-width: revert; |
| 1165 | 1166 | } |
| 1166 | - | |
| 1167 | -body.chat .message-widget .message-widget-tab { | |
| 1168 | - /* Make /chat user names and timestamps more visible */ | |
| 1169 | - filter: saturate(6); | |
| 1167 | +body.chat #chat-user-list .chat-user{ | |
| 1168 | + color: white; | |
| 1170 | 1169 | } |
| 1171 | 1170 |
| --- skins/xekri/css.txt | |
| +++ skins/xekri/css.txt | |
| @@ -237,11 +237,11 @@ | |
| 237 | div.footer div { |
| 238 | background-color: #222; |
| 239 | box-shadow: 3px 3px 1px #000; |
| 240 | border-radius: 0 0 1rem 1rem; |
| 241 | margin: 0 0 10px 0; |
| 242 | padding: 0.5rem 0.75rem; |
| 243 | } |
| 244 | |
| 245 | div.footer div.page-time { |
| 246 | float: left; |
| 247 | } |
| @@ -1157,14 +1157,13 @@ | |
| 1157 | body.chat div.header, body.chat div.footer, |
| 1158 | body.chat div.mainmenu, body.chat div.submenu, |
| 1159 | body.chat div.content { |
| 1160 | margin-left: auto; |
| 1161 | margin-right: auto; |
| 1162 | } |
| 1163 | body.chat.chat-only-mode div.content { |
| 1164 | max-width: revert; |
| 1165 | } |
| 1166 | |
| 1167 | body.chat .message-widget .message-widget-tab { |
| 1168 | /* Make /chat user names and timestamps more visible */ |
| 1169 | filter: saturate(6); |
| 1170 | } |
| 1171 |
| --- skins/xekri/css.txt | |
| +++ skins/xekri/css.txt | |
| @@ -237,11 +237,11 @@ | |
| 237 | div.footer div { |
| 238 | background-color: #222; |
| 239 | box-shadow: 3px 3px 1px #000; |
| 240 | border-radius: 0 0 1rem 1rem; |
| 241 | margin: 0 0 10px 0; |
| 242 | padding: 0.25rem 0.75rem; |
| 243 | } |
| 244 | |
| 245 | div.footer div.page-time { |
| 246 | float: left; |
| 247 | } |
| @@ -1157,14 +1157,13 @@ | |
| 1157 | body.chat div.header, body.chat div.footer, |
| 1158 | body.chat div.mainmenu, body.chat div.submenu, |
| 1159 | body.chat div.content { |
| 1160 | margin-left: auto; |
| 1161 | margin-right: auto; |
| 1162 | margin-top: auto/*eliminates unnecessary scrollbars*/; |
| 1163 | } |
| 1164 | body.chat.chat-only-mode div.content { |
| 1165 | max-width: revert; |
| 1166 | } |
| 1167 | body.chat #chat-user-list .chat-user{ |
| 1168 | color: white; |
| 1169 | } |
| 1170 |
+1
-1
| --- src/builtin.c | ||
| +++ src/builtin.c | ||
| @@ -704,11 +704,11 @@ | ||
| 704 | 704 | ** the final one! */ |
| 705 | 705 | } fjs[] = { |
| 706 | 706 | /* This list ordering isn't strictly important. */ |
| 707 | 707 | {"confirmer", 0, 0}, |
| 708 | 708 | {"copybutton", 0, "dom\0"}, |
| 709 | - {"diff", 0, "dom\0fetch\0popupwidget\0"}, | |
| 709 | + {"diff", 0, "dom\0fetch\0"}, | |
| 710 | 710 | {"dom", 0, 0}, |
| 711 | 711 | {"fetch", 0, 0}, |
| 712 | 712 | {"numbered-lines", 0, "popupwidget\0copybutton\0"}, |
| 713 | 713 | {"pikchr", 0, "dom\0"}, |
| 714 | 714 | {"popupwidget", 0, "dom\0"}, |
| 715 | 715 |
| --- src/builtin.c | |
| +++ src/builtin.c | |
| @@ -704,11 +704,11 @@ | |
| 704 | ** the final one! */ |
| 705 | } fjs[] = { |
| 706 | /* This list ordering isn't strictly important. */ |
| 707 | {"confirmer", 0, 0}, |
| 708 | {"copybutton", 0, "dom\0"}, |
| 709 | {"diff", 0, "dom\0fetch\0popupwidget\0"}, |
| 710 | {"dom", 0, 0}, |
| 711 | {"fetch", 0, 0}, |
| 712 | {"numbered-lines", 0, "popupwidget\0copybutton\0"}, |
| 713 | {"pikchr", 0, "dom\0"}, |
| 714 | {"popupwidget", 0, "dom\0"}, |
| 715 |
| --- src/builtin.c | |
| +++ src/builtin.c | |
| @@ -704,11 +704,11 @@ | |
| 704 | ** the final one! */ |
| 705 | } fjs[] = { |
| 706 | /* This list ordering isn't strictly important. */ |
| 707 | {"confirmer", 0, 0}, |
| 708 | {"copybutton", 0, "dom\0"}, |
| 709 | {"diff", 0, "dom\0fetch\0"}, |
| 710 | {"dom", 0, 0}, |
| 711 | {"fetch", 0, 0}, |
| 712 | {"numbered-lines", 0, "popupwidget\0copybutton\0"}, |
| 713 | {"pikchr", 0, "dom\0"}, |
| 714 | {"popupwidget", 0, "dom\0"}, |
| 715 |
+16
-3
| --- src/chat.c | ||
| +++ src/chat.c | ||
| @@ -181,21 +181,34 @@ | ||
| 181 | 181 | @ <input type="file" name="file" id="chat-input-file"> |
| 182 | 182 | @ </div> |
| 183 | 183 | @ <div id="chat-drop-details"></div> |
| 184 | 184 | @ </div> |
| 185 | 185 | @ </div> |
| 186 | - @ <div id='chat-preview' class='hidden'> | |
| 186 | + @ <div id='chat-user-list-wrapper' class='hidden'> | |
| 187 | + @ <div class='legend'> | |
| 188 | + @ <span class='help-buttonlet'> | |
| 189 | + @ Users who have messages in the currently-loaded list.<br><br> | |
| 190 | + @ <strong>Tap a user name</strong> to filter messages | |
| 191 | + @ on that user and tap again to clear the filter.<br><br> | |
| 192 | + @ <strong>Tap the title</strong> of this widget to toggle | |
| 193 | + @ the list on and off. | |
| 194 | + @ </span> | |
| 195 | + @ <span>Active users (sorted by last message time)</span> | |
| 196 | + @ </div> | |
| 197 | + @ <div id='chat-user-list'></div> | |
| 198 | + @ </div> | |
| 199 | + @ <div id='chat-preview' class='hidden chat-view'> | |
| 187 | 200 | @ <header>Preview: (<a href='%R/md_rules' target='_blank'>markdown reference</a>)</header> |
| 188 | 201 | @ <div id='chat-preview-content' class='message-widget-content'></div> |
| 189 | 202 | @ <div id='chat-preview-buttons'><button id='chat-preview-close'>Close Preview</button></div> |
| 190 | 203 | @ </div> |
| 191 | - @ <div id='chat-config' class='hidden'> | |
| 204 | + @ <div id='chat-config' class='hidden chat-view'> | |
| 192 | 205 | @ <div id='chat-config-options'></div> |
| 193 | 206 | /* ^^^populated client-side */ |
| 194 | 207 | @ <button>Close Settings</button> |
| 195 | 208 | @ </div> |
| 196 | - @ <div id='chat-messages-wrapper'> | |
| 209 | + @ <div id='chat-messages-wrapper' class='chat-view'> | |
| 197 | 210 | /* New chat messages get inserted immediately after this element */ |
| 198 | 211 | @ <span id='message-inject-point'></span> |
| 199 | 212 | @ </div> |
| 200 | 213 | fossil_free(zProjectName); |
| 201 | 214 | builtin_fossil_js_bundle_or("popupwidget", "storage", "fetch", |
| 202 | 215 |
| --- src/chat.c | |
| +++ src/chat.c | |
| @@ -181,21 +181,34 @@ | |
| 181 | @ <input type="file" name="file" id="chat-input-file"> |
| 182 | @ </div> |
| 183 | @ <div id="chat-drop-details"></div> |
| 184 | @ </div> |
| 185 | @ </div> |
| 186 | @ <div id='chat-preview' class='hidden'> |
| 187 | @ <header>Preview: (<a href='%R/md_rules' target='_blank'>markdown reference</a>)</header> |
| 188 | @ <div id='chat-preview-content' class='message-widget-content'></div> |
| 189 | @ <div id='chat-preview-buttons'><button id='chat-preview-close'>Close Preview</button></div> |
| 190 | @ </div> |
| 191 | @ <div id='chat-config' class='hidden'> |
| 192 | @ <div id='chat-config-options'></div> |
| 193 | /* ^^^populated client-side */ |
| 194 | @ <button>Close Settings</button> |
| 195 | @ </div> |
| 196 | @ <div id='chat-messages-wrapper'> |
| 197 | /* New chat messages get inserted immediately after this element */ |
| 198 | @ <span id='message-inject-point'></span> |
| 199 | @ </div> |
| 200 | fossil_free(zProjectName); |
| 201 | builtin_fossil_js_bundle_or("popupwidget", "storage", "fetch", |
| 202 |
| --- src/chat.c | |
| +++ src/chat.c | |
| @@ -181,21 +181,34 @@ | |
| 181 | @ <input type="file" name="file" id="chat-input-file"> |
| 182 | @ </div> |
| 183 | @ <div id="chat-drop-details"></div> |
| 184 | @ </div> |
| 185 | @ </div> |
| 186 | @ <div id='chat-user-list-wrapper' class='hidden'> |
| 187 | @ <div class='legend'> |
| 188 | @ <span class='help-buttonlet'> |
| 189 | @ Users who have messages in the currently-loaded list.<br><br> |
| 190 | @ <strong>Tap a user name</strong> to filter messages |
| 191 | @ on that user and tap again to clear the filter.<br><br> |
| 192 | @ <strong>Tap the title</strong> of this widget to toggle |
| 193 | @ the list on and off. |
| 194 | @ </span> |
| 195 | @ <span>Active users (sorted by last message time)</span> |
| 196 | @ </div> |
| 197 | @ <div id='chat-user-list'></div> |
| 198 | @ </div> |
| 199 | @ <div id='chat-preview' class='hidden chat-view'> |
| 200 | @ <header>Preview: (<a href='%R/md_rules' target='_blank'>markdown reference</a>)</header> |
| 201 | @ <div id='chat-preview-content' class='message-widget-content'></div> |
| 202 | @ <div id='chat-preview-buttons'><button id='chat-preview-close'>Close Preview</button></div> |
| 203 | @ </div> |
| 204 | @ <div id='chat-config' class='hidden chat-view'> |
| 205 | @ <div id='chat-config-options'></div> |
| 206 | /* ^^^populated client-side */ |
| 207 | @ <button>Close Settings</button> |
| 208 | @ </div> |
| 209 | @ <div id='chat-messages-wrapper' class='chat-view'> |
| 210 | /* New chat messages get inserted immediately after this element */ |
| 211 | @ <span id='message-inject-point'></span> |
| 212 | @ </div> |
| 213 | fossil_free(zProjectName); |
| 214 | builtin_fossil_js_bundle_or("popupwidget", "storage", "fetch", |
| 215 |
+360
-106
| --- src/chat.js | ||
| +++ src/chat.js | ||
| @@ -34,10 +34,30 @@ | ||
| 34 | 34 | else if(r1.bottom<=r2.bottom && r1.bottom>=r2.top) return true; |
| 35 | 35 | return false; |
| 36 | 36 | }; |
| 37 | 37 | |
| 38 | 38 | const addAnchorTargetBlank = (e)=>D.attr(e, 'target','_blank'); |
| 39 | + | |
| 40 | + /** | |
| 41 | + Returns an almost-ISO8601 form of Date object d. | |
| 42 | + */ | |
| 43 | + const iso8601ish = function(d){ | |
| 44 | + return d.toISOString() | |
| 45 | + .replace('T',' ').replace(/\.\d+/,'') | |
| 46 | + .replace('Z', ' zulu'); | |
| 47 | + }; | |
| 48 | + /** Returns the local time string of Date object d, defaulting | |
| 49 | + to the current time. */ | |
| 50 | + const localTimeString = function ff(d){ | |
| 51 | + d || (d = new Date()); | |
| 52 | + return [ | |
| 53 | + d.getFullYear(),'-',pad2(d.getMonth()+1/*sigh*/), | |
| 54 | + '-',pad2(d.getDate()), | |
| 55 | + ' ',pad2(d.getHours()),':',pad2(d.getMinutes()), | |
| 56 | + ':',pad2(d.getSeconds()) | |
| 57 | + ].join(''); | |
| 58 | + }; | |
| 39 | 59 | |
| 40 | 60 | (function(){ |
| 41 | 61 | let dbg = document.querySelector('#debugMsg'); |
| 42 | 62 | if(dbg){ |
| 43 | 63 | /* This can inadvertently influence our flexbox layouts, so move |
| @@ -74,11 +94,11 @@ | ||
| 74 | 94 | ht = wh - extra; |
| 75 | 95 | } |
| 76 | 96 | f.contentArea.style.height = |
| 77 | 97 | f.contentArea.style.maxHeight = [ |
| 78 | 98 | "calc(", (ht>=100 ? ht : 100), "px", |
| 79 | - " - 1em"/*fudge value*/,")" | |
| 99 | + " - 0.75em"/*fudge value*/,")" | |
| 80 | 100 | /* ^^^^ hypothetically not needed, but both Chrome/FF on |
| 81 | 101 | Linux will force scrollbars on the body if this value is |
| 82 | 102 | too small (<0.75em in my tests). */ |
| 83 | 103 | ].join(''); |
| 84 | 104 | if(false){ |
| @@ -105,21 +125,24 @@ | ||
| 105 | 125 | pageTitle: E1('head title'), |
| 106 | 126 | loadOlderToolbar: undefined /* the load-posts toolbar (dynamically created) */, |
| 107 | 127 | inputWrapper: E1("#chat-input-area"), |
| 108 | 128 | inputLine: E1('#chat-input-line'), |
| 109 | 129 | fileSelectWrapper: E1('#chat-input-file-area'), |
| 110 | - messagesWrapper: E1('#chat-messages-wrapper'), | |
| 130 | + viewMessages: E1('#chat-messages-wrapper'), | |
| 111 | 131 | btnSubmit: E1('#chat-message-submit'), |
| 112 | 132 | inputSingle: E1('#chat-input-single'), |
| 113 | 133 | inputMulti: E1('#chat-input-multi'), |
| 114 | 134 | inputCurrent: undefined/*one of inputSingle or inputMulti*/, |
| 115 | 135 | inputFile: E1('#chat-input-file'), |
| 116 | 136 | contentDiv: E1('div.content'), |
| 117 | - configArea: E1('#chat-config'), | |
| 118 | - previewArea: E1('#chat-preview'), | |
| 137 | + viewConfig: E1('#chat-config'), | |
| 138 | + viewPreview: E1('#chat-preview'), | |
| 119 | 139 | previewContent: E1('#chat-preview-content'), |
| 120 | - btnPreview: E1('#chat-preview-button') | |
| 140 | + btnPreview: E1('#chat-preview-button'), | |
| 141 | + views: document.querySelectorAll('.chat-view'), | |
| 142 | + activeUserListWrapper: E1('#chat-user-list-wrapper'), | |
| 143 | + activeUserList: E1('#chat-user-list') | |
| 121 | 144 | }, |
| 122 | 145 | me: F.user.name, |
| 123 | 146 | mxMsg: F.config.chat.initSize ? -F.config.chat.initSize : -50, |
| 124 | 147 | mnMsg: undefined/*lowest message ID we've seen so far (for history loading)*/, |
| 125 | 148 | pageIsActive: 'visible'===document.visibilityState, |
| @@ -127,10 +150,23 @@ | ||
| 127 | 150 | notificationBubbleColor: 'white', |
| 128 | 151 | totalMessageCount: 0, // total # of inbound messages |
| 129 | 152 | //! Number of messages to load for the history buttons |
| 130 | 153 | loadMessageCount: Math.abs(F.config.chat.initSize || 20), |
| 131 | 154 | ajaxInflight: 0, |
| 155 | + usersLastSeen:{ | |
| 156 | + /* Map of user names to their most recent message time | |
| 157 | + (JS Date object). Only messages received by the chat client | |
| 158 | + are considered. */ | |
| 159 | + /* Reminder: to convert a Julian time J to JS: | |
| 160 | + new Date((J - 2440587.5) * 86400000) */ | |
| 161 | + }, | |
| 162 | + filterState:{ | |
| 163 | + activeUser: undefined, | |
| 164 | + match: function(uname){ | |
| 165 | + return this.activeUser===uname || !this.activeUser; | |
| 166 | + } | |
| 167 | + }, | |
| 132 | 168 | /** Gets (no args) or sets (1 arg) the current input text field value, |
| 133 | 169 | taking into account single- vs multi-line input. The getter returns |
| 134 | 170 | a string and the setter returns this object. */ |
| 135 | 171 | inputValue: function(){ |
| 136 | 172 | const e = this.inputElement(); |
| @@ -157,19 +193,20 @@ | ||
| 157 | 193 | this.e.inputLine.classList.remove('single-line'); |
| 158 | 194 | }else{ |
| 159 | 195 | this.e.inputCurrent = this.e.inputSingle; |
| 160 | 196 | this.e.inputLine.classList.add('single-line'); |
| 161 | 197 | } |
| 162 | - const m = this.e.messagesWrapper, | |
| 198 | + const m = this.e.viewMessages, | |
| 163 | 199 | sTop = m.scrollTop, |
| 164 | 200 | mh1 = m.clientHeight; |
| 165 | 201 | D.addClass(old, 'hidden'); |
| 166 | 202 | D.removeClass(this.e.inputCurrent, 'hidden'); |
| 167 | 203 | const mh2 = m.clientHeight; |
| 168 | 204 | m.scrollTo(0, sTop + (mh1-mh2)); |
| 169 | 205 | this.e.inputCurrent.value = old.value; |
| 170 | 206 | old.value = ''; |
| 207 | + this.animate(this.e.inputCurrent, "anim-flip-v"); | |
| 171 | 208 | return this; |
| 172 | 209 | }, |
| 173 | 210 | /** |
| 174 | 211 | If passed true or no arguments, switches to multi-line mode |
| 175 | 212 | if currently in single-line mode. If passed false, switches |
| @@ -241,12 +278,15 @@ | ||
| 241 | 278 | /* Injects DOM element e as a new row in the chat, at the oldest |
| 242 | 279 | end of the list if atEnd is truthy, else at the newest end of |
| 243 | 280 | the list. */ |
| 244 | 281 | injectMessageElem: function f(e, atEnd){ |
| 245 | 282 | const mip = atEnd ? this.e.loadOlderToolbar : this.e.messageInjectPoint, |
| 246 | - holder = this.e.messagesWrapper, | |
| 283 | + holder = this.e.viewMessages, | |
| 247 | 284 | prevMessage = this.e.newestMessage; |
| 285 | + if(!this.filterState.match(e.dataset.xfrom)){ | |
| 286 | + e.classList.add('hidden'); | |
| 287 | + } | |
| 248 | 288 | if(atEnd){ |
| 249 | 289 | const fe = mip.nextElementSibling; |
| 250 | 290 | if(fe) mip.parentNode.insertBefore(e, fe); |
| 251 | 291 | else D.append(mip.parentNode, e); |
| 252 | 292 | }else{ |
| @@ -337,22 +377,22 @@ | ||
| 337 | 377 | <0 = top of the message list, >0 = bottom of the message list, |
| 338 | 378 | 0 == the newest message (normally the same position as >1). |
| 339 | 379 | */ |
| 340 | 380 | scrollMessagesTo: function(where){ |
| 341 | 381 | if(where<0){ |
| 342 | - Chat.e.messagesWrapper.scrollTop = 0; | |
| 382 | + Chat.e.viewMessages.scrollTop = 0; | |
| 343 | 383 | }else if(where>0){ |
| 344 | - Chat.e.messagesWrapper.scrollTop = Chat.e.messagesWrapper.scrollHeight; | |
| 384 | + Chat.e.viewMessages.scrollTop = Chat.e.viewMessages.scrollHeight; | |
| 345 | 385 | }else if(Chat.e.newestMessage){ |
| 346 | 386 | Chat.e.newestMessage.scrollIntoView(false); |
| 347 | 387 | } |
| 348 | 388 | }, |
| 349 | 389 | toggleChatOnlyMode: function(){ |
| 350 | 390 | return this.chatOnlyMode(!this.isChatOnlyMode()); |
| 351 | 391 | }, |
| 352 | 392 | messageIsInView: function(e){ |
| 353 | - return e ? overlapsElemView(e, this.e.messagesWrapper) : false; | |
| 393 | + return e ? overlapsElemView(e, this.e.viewMessages) : false; | |
| 354 | 394 | }, |
| 355 | 395 | settings:{ |
| 356 | 396 | get: (k,dflt)=>F.storage.get(k,dflt), |
| 357 | 397 | getBool: (k,dflt)=>F.storage.getBool(k,dflt), |
| 358 | 398 | set: (k,v)=>F.storage.set(k,v), |
| @@ -366,11 +406,13 @@ | ||
| 366 | 406 | defaults:{ |
| 367 | 407 | "images-inline": !!F.config.chat.imagesInline, |
| 368 | 408 | "edit-multiline": false, |
| 369 | 409 | "monospace-messages": false, |
| 370 | 410 | "chat-only-mode": false, |
| 371 | - "audible-alert": true | |
| 411 | + "audible-alert": true, | |
| 412 | + "active-user-list": false, | |
| 413 | + "active-user-list-timestamps": false | |
| 372 | 414 | } |
| 373 | 415 | }, |
| 374 | 416 | /** Plays a new-message notification sound IF the audible-alert |
| 375 | 417 | setting is true, else this is a no-op. Returns this. |
| 376 | 418 | */ |
| @@ -395,12 +437,117 @@ | ||
| 395 | 437 | setNewMessageSound: function f(uri){ |
| 396 | 438 | delete this.playNewMessageSound.audio; |
| 397 | 439 | this.playNewMessageSound.uri = uri; |
| 398 | 440 | this.settings.set('audible-alert', uri); |
| 399 | 441 | return this; |
| 442 | + }, | |
| 443 | + /** | |
| 444 | + Expects e to be one of the elements in this.e.views. | |
| 445 | + The 'hidden' class is removed from e and added to | |
| 446 | + all other elements in that list. Returns e. | |
| 447 | + */ | |
| 448 | + setCurrentView: function(e){ | |
| 449 | + if(e===this.e.currentView){ | |
| 450 | + return e; | |
| 451 | + } | |
| 452 | + this.e.views.forEach(function(E){ | |
| 453 | + if(e!==E) D.addClass(E,'hidden'); | |
| 454 | + }); | |
| 455 | + this.e.currentView = D.removeClass(e,'hidden'); | |
| 456 | + this.animate(this.e.currentView, 'anim-fade-in-fast'); | |
| 457 | + return this.e.currentView; | |
| 458 | + }, | |
| 459 | + /** | |
| 460 | + Updates the "active user list" view if we are not currently | |
| 461 | + batch-loading messages and if the active user list UI element | |
| 462 | + is active. | |
| 463 | + */ | |
| 464 | + updateActiveUserList: function callee(){ | |
| 465 | + if(this._isBatchLoading | |
| 466 | + || this.e.activeUserListWrapper.classList.contains('hidden')){ | |
| 467 | + return this; | |
| 468 | + }else if(!callee.sortUsersSeen){ | |
| 469 | + /** Array.sort() callback. Expects an array of user names and | |
| 470 | + sorts them in last-received message order (newest first). */ | |
| 471 | + const self = this; | |
| 472 | + callee.sortUsersSeen = function(l,r){ | |
| 473 | + l = self.usersLastSeen[l]; | |
| 474 | + r = self.usersLastSeen[r]; | |
| 475 | + if(l && r) return r - l; | |
| 476 | + else if(l) return -1; | |
| 477 | + else if(r) return 1; | |
| 478 | + else return 0; | |
| 479 | + }; | |
| 480 | + callee.addUserElem = function(u){ | |
| 481 | + const uSpan = D.addClass(D.span(), 'chat-user'); | |
| 482 | + const uDate = self.usersLastSeen[u]; | |
| 483 | + if(self.filterState.activeUser===u){ | |
| 484 | + uSpan.classList.add('selected'); | |
| 485 | + } | |
| 486 | + uSpan.dataset.uname = u; | |
| 487 | + D.append(uSpan, u, "\n", | |
| 488 | + D.append( | |
| 489 | + D.addClass(D.span(),'timestamp'), | |
| 490 | + localTimeString(uDate)//.substr(5/*chop off year*/) | |
| 491 | + )); | |
| 492 | + if(uDate.$uColor){ | |
| 493 | + uSpan.style.backgroundColor = uDate.$uColor; | |
| 494 | + } | |
| 495 | + D.append(self.e.activeUserList, uSpan); | |
| 496 | + }; | |
| 497 | + } | |
| 498 | + //D.clearElement(this.e.activeUserList); | |
| 499 | + D.remove(this.e.activeUserList.querySelectorAll('.chat-user')); | |
| 500 | + Object.keys(this.usersLastSeen).sort( | |
| 501 | + callee.sortUsersSeen | |
| 502 | + ).forEach(callee.addUserElem); | |
| 503 | + return this; | |
| 504 | + }, | |
| 505 | + /** | |
| 506 | + Applies user name filter to all current messages, or clears | |
| 507 | + the filter if uname is falsy. | |
| 508 | + */ | |
| 509 | + setUserFilter: function(uname){ | |
| 510 | + this.filterState.activeUser = uname; | |
| 511 | + const mw = this.e.viewMessages.querySelectorAll('.message-widget'); | |
| 512 | + const self = this; | |
| 513 | + let eLast; | |
| 514 | + if(!uname){ | |
| 515 | + D.removeClass(Chat.e.viewMessages.querySelectorAll('.message-widget.hidden'), | |
| 516 | + 'hidden'); | |
| 517 | + }else{ | |
| 518 | + mw.forEach(function(w){ | |
| 519 | + if(self.filterState.match(w.dataset.xfrom)){ | |
| 520 | + w.classList.remove('hidden'); | |
| 521 | + eLast = w; | |
| 522 | + }else{ | |
| 523 | + w.classList.add('hidden'); | |
| 524 | + } | |
| 525 | + }); | |
| 526 | + } | |
| 527 | + if(eLast) eLast.scrollIntoView(false); | |
| 528 | + else this.scrollMessagesTo(1); | |
| 529 | + cs.e.activeUserList.querySelectorAll('.chat-user').forEach(function(e){ | |
| 530 | + e.classList[uname===e.dataset.uname ? 'add' : 'remove']('selected'); | |
| 531 | + }); | |
| 532 | + return this; | |
| 533 | + }, | |
| 534 | + | |
| 535 | + /** | |
| 536 | + If animations are enabled, passes its arguments | |
| 537 | + to D.addClassBriefly(), else this is a no-op. | |
| 538 | + If cb is a function, it is called after the | |
| 539 | + CSS class is removed. Returns this object; | |
| 540 | + */ | |
| 541 | + animate: function f(e,a,cb){ | |
| 542 | + if(!f.$disabled){ | |
| 543 | + D.addClassBriefly(e, a, 0, cb); | |
| 544 | + } | |
| 545 | + return this; | |
| 400 | 546 | } |
| 401 | 547 | }; |
| 548 | + cs.animate.$disabled = true; | |
| 402 | 549 | F.fetch.beforesend = ()=>cs.ajaxStart(); |
| 403 | 550 | F.fetch.aftersend = ()=>cs.ajaxEnd(); |
| 404 | 551 | cs.e.inputCurrent = cs.e.inputSingle; |
| 405 | 552 | /* Install default settings... */ |
| 406 | 553 | Object.keys(cs.settings.defaults).forEach(function(k){ |
| @@ -415,10 +562,16 @@ | ||
| 415 | 562 | tall vs wide. Can be toggled via settings popup. */ |
| 416 | 563 | document.body.classList.add('my-messages-right'); |
| 417 | 564 | } |
| 418 | 565 | if(cs.settings.getBool('monospace-messages',false)){ |
| 419 | 566 | document.body.classList.add('monospace-messages'); |
| 567 | + } | |
| 568 | + if(cs.settings.getBool('active-user-list',false)){ | |
| 569 | + cs.e.activeUserListWrapper.classList.remove('hidden'); | |
| 570 | + } | |
| 571 | + if(cs.settings.getBool('active-user-list-timestamps',false)){ | |
| 572 | + cs.e.activeUserList.classList.add('timestamps'); | |
| 420 | 573 | } |
| 421 | 574 | cs.inputMultilineMode(cs.settings.getBool('edit-multiline',false)); |
| 422 | 575 | cs.chatOnlyMode(cs.settings.getBool('chat-only-mode')); |
| 423 | 576 | cs.pageTitleOrig = cs.e.pageTitle.innerText; |
| 424 | 577 | const qs = (e)=>document.querySelector(e); |
| @@ -611,10 +764,37 @@ | ||
| 611 | 764 | cs.pageIsActive = ('visible' === document.visibilityState); |
| 612 | 765 | if(cs.pageIsActive){ |
| 613 | 766 | cs.e.pageTitle.innerText = cs.pageTitleOrig; |
| 614 | 767 | } |
| 615 | 768 | }, true); |
| 769 | + cs.setCurrentView(cs.e.viewMessages); | |
| 770 | + | |
| 771 | + cs.e.activeUserList.addEventListener('click', function f(ev){ | |
| 772 | + /* Filter messages on a user clicked in activeUserList */ | |
| 773 | + ev.stopPropagation(); | |
| 774 | + ev.preventDefault(); | |
| 775 | + let eUser = ev.target; | |
| 776 | + while(eUser!==this && !eUser.classList.contains('chat-user')){ | |
| 777 | + eUser = eUser.parentNode; | |
| 778 | + } | |
| 779 | + if(eUser==this || !eUser) return false; | |
| 780 | + const uname = eUser.dataset.uname; | |
| 781 | + let eLast; | |
| 782 | + cs.setCurrentView(cs.e.viewMessages); | |
| 783 | + if(eUser.classList.contains('selected')){ | |
| 784 | + /* If curently selected, toggle filter off */ | |
| 785 | + eUser.classList.remove('selected'); | |
| 786 | + cs.setUserFilter(false); | |
| 787 | + delete f.$eSelected; | |
| 788 | + }else{ | |
| 789 | + if(f.$eSelected) f.$eSelected.classList.remove('selected'); | |
| 790 | + f.$eSelected = eUser; | |
| 791 | + eUser.classList.add('selected'); | |
| 792 | + cs.setUserFilter(uname); | |
| 793 | + } | |
| 794 | + return false; | |
| 795 | + }, false); | |
| 616 | 796 | return cs; |
| 617 | 797 | })()/*Chat initialization*/; |
| 618 | 798 | |
| 619 | 799 | /** |
| 620 | 800 | Custom widget type for rendering messages (one message per |
| @@ -658,21 +838,10 @@ | ||
| 658 | 838 | d.getHours(),":", |
| 659 | 839 | (d.getMinutes()+100).toString().slice(1,3), |
| 660 | 840 | ' ', dowMap[d.getDay()] |
| 661 | 841 | ].join(''); |
| 662 | 842 | }; |
| 663 | - /** Returns the local time string of Date object d, defaulting | |
| 664 | - to the current time. */ | |
| 665 | - const localTimeString = function ff(d){ | |
| 666 | - d || (d = new Date()); | |
| 667 | - return [ | |
| 668 | - d.getFullYear(),'-',pad2(d.getMonth()+1/*sigh*/), | |
| 669 | - '-',pad2(d.getDate()), | |
| 670 | - ' ',pad2(d.getHours()),':',pad2(d.getMinutes()), | |
| 671 | - ':',pad2(d.getSeconds()) | |
| 672 | - ].join(''); | |
| 673 | - }; | |
| 674 | 843 | cf.prototype = { |
| 675 | 844 | scrollIntoView: function(){ |
| 676 | 845 | this.e.content.scrollIntoView(); |
| 677 | 846 | }, |
| 678 | 847 | setMessage: function(m){ |
| @@ -758,14 +927,14 @@ | ||
| 758 | 927 | eXFrom.addEventListener('click', ()=>this.e.tab.click(), false); |
| 759 | 928 | }*/ |
| 760 | 929 | return this; |
| 761 | 930 | }, |
| 762 | 931 | /* Event handler for clicking .message-user elements to show their |
| 763 | - timestamps. */ | |
| 932 | + timestamps and a set of actions. */ | |
| 764 | 933 | _handleLegendClicked: function f(ev){ |
| 765 | 934 | if(!f.popup){ |
| 766 | - /* Timestamp popup widget */ | |
| 935 | + /* "Popup" widget */ | |
| 767 | 936 | f.popup = { |
| 768 | 937 | e: D.addClass(D.div(), 'chat-message-popup'), |
| 769 | 938 | refresh:function(){ |
| 770 | 939 | const eMsg = this.$eMsg/*.message-widget element*/; |
| 771 | 940 | if(!eMsg) return; |
| @@ -830,18 +999,45 @@ | ||
| 830 | 999 | y: 'a' |
| 831 | 1000 | }), "User's Timeline"), |
| 832 | 1001 | 'target', '_blank' |
| 833 | 1002 | ); |
| 834 | 1003 | D.append(toolbar2, timelineLink); |
| 1004 | + if(Chat.filterState.activeUser && | |
| 1005 | + Chat.filterState.match(eMsg.dataset.xfrom)){ | |
| 1006 | + /* Add a button to clear user filter and jump to | |
| 1007 | + this message in its original context. */ | |
| 1008 | + D.append( | |
| 1009 | + this.e, | |
| 1010 | + D.append( | |
| 1011 | + D.addClass(D.div(), 'toolbar'), | |
| 1012 | + D.button( | |
| 1013 | + "Message in context", | |
| 1014 | + function(){ | |
| 1015 | + self.hide(); | |
| 1016 | + Chat.setUserFilter(false); | |
| 1017 | + eMsg.scrollIntoView(false); | |
| 1018 | + Chat.animate( | |
| 1019 | + eMsg.firstElementChild, 'anim-flip-h' | |
| 1020 | + //eMsg.firstElementChild, 'anim-flip-v' | |
| 1021 | + //eMsg.childNodes, 'anim-rotate-360' | |
| 1022 | + //eMsg.childNodes, 'anim-flip-v' | |
| 1023 | + //eMsg, 'anim-flip-v' | |
| 1024 | + ); | |
| 1025 | + }) | |
| 1026 | + ) | |
| 1027 | + ); | |
| 1028 | + }/*jump-to button*/ | |
| 835 | 1029 | } |
| 836 | 1030 | const tab = eMsg.querySelector('.message-widget-tab'); |
| 837 | 1031 | D.append(tab, this.e); |
| 838 | 1032 | D.removeClass(this.e, 'hidden'); |
| 1033 | + Chat.animate(this.e, 'anim-fade-in-fast'); | |
| 839 | 1034 | }/*refresh()*/, |
| 840 | 1035 | hide: function(){ |
| 841 | - D.addClass(D.clearElement(this.e), 'hidden'); | |
| 842 | 1036 | delete this.$eMsg; |
| 1037 | + D.addClass(this.e, 'hidden'); | |
| 1038 | + D.clearElement(this.e); | |
| 843 | 1039 | }, |
| 844 | 1040 | show: function(tgtMsg){ |
| 845 | 1041 | if(tgtMsg === this.$eMsg){ |
| 846 | 1042 | this.hide(); |
| 847 | 1043 | return; |
| @@ -961,11 +1157,11 @@ | ||
| 961 | 1157 | */ |
| 962 | 1158 | Chat.submitMessage = function f(){ |
| 963 | 1159 | if(!f.spaces){ |
| 964 | 1160 | f.spaces = /\s+$/; |
| 965 | 1161 | } |
| 966 | - this.revealPreview(false); | |
| 1162 | + this.setCurrentView(this.e.viewMessages); | |
| 967 | 1163 | const fd = new FormData(); |
| 968 | 1164 | var msg = this.inputValue().trim(); |
| 969 | 1165 | if(msg && (msg.indexOf('\n')>0 || f.spaces.test(msg))){ |
| 970 | 1166 | /* Cosmetic: trim whitespace from the ends of lines to try to |
| 971 | 1167 | keep copy/paste from terminals, especially wide ones, from |
| @@ -1021,42 +1217,102 @@ | ||
| 1021 | 1217 | e.preventDefault(); |
| 1022 | 1218 | Chat.submitMessage(); |
| 1023 | 1219 | return false; |
| 1024 | 1220 | }); |
| 1025 | 1221 | |
| 1026 | - /* Returns an almost-ISO8601 form of Date object d. */ | |
| 1027 | - const iso8601ish = function(d){ | |
| 1028 | - return d.toISOString() | |
| 1029 | - .replace('T',' ').replace(/\.\d+/,'').replace('Z', ' zulu'); | |
| 1030 | - }; | |
| 1031 | - | |
| 1032 | 1222 | (function(){/*Set up #chat-settings-button */ |
| 1033 | 1223 | const settingsButton = document.querySelector('#chat-settings-button'); |
| 1034 | 1224 | const optionsMenu = E1('#chat-config-options'); |
| 1035 | 1225 | const cbToggle = function(ev){ |
| 1036 | 1226 | ev.preventDefault(); |
| 1037 | 1227 | ev.stopPropagation(); |
| 1038 | - if(Chat.e.configArea.classList.contains('hidden')){ | |
| 1039 | - D.removeClass(Chat.e.configArea, 'hidden'); | |
| 1040 | - D.addClass([Chat.e.messagesWrapper, Chat.e.previewArea], 'hidden'); | |
| 1041 | - }else{ | |
| 1042 | - D.addClass(Chat.e.configArea, 'hidden'); | |
| 1043 | - D.removeClass(Chat.e.messagesWrapper, 'hidden'); | |
| 1044 | - } | |
| 1228 | + Chat.setCurrentView(Chat.e.currentView===Chat.e.viewConfig | |
| 1229 | + ? Chat.e.viewMessages : Chat.e.viewConfig); | |
| 1045 | 1230 | return false; |
| 1046 | 1231 | }; |
| 1047 | 1232 | D.attr(settingsButton, 'role', 'button').addEventListener('click', cbToggle, false); |
| 1048 | - Chat.e.configArea.querySelector('button').addEventListener('click', cbToggle, false); | |
| 1049 | - /* Settings menu entries... */ | |
| 1233 | + Chat.e.viewConfig.querySelector('button').addEventListener('click', cbToggle, false); | |
| 1234 | + | |
| 1235 | + /** Internal acrobatics to allow certain settings toggles to access | |
| 1236 | + related toggles. */ | |
| 1237 | + const namedOptions = { | |
| 1238 | + activeUsers:{ | |
| 1239 | + label: "Show active users list", | |
| 1240 | + boolValue: ()=>!Chat.e.activeUserListWrapper.classList.contains('hidden'), | |
| 1241 | + persistentSetting: 'active-user-list', | |
| 1242 | + callback: function(){ | |
| 1243 | + D.toggleClass(Chat.e.activeUserListWrapper,'hidden'); | |
| 1244 | + D.removeClass(Chat.e.activeUserListWrapper, 'collapsed'); | |
| 1245 | + if(Chat.e.activeUserListWrapper.classList.contains('hidden')){ | |
| 1246 | + /* When hiding this element, undo all filtering */ | |
| 1247 | + Chat.setUserFilter(false); | |
| 1248 | + /*Ideally we'd scroll the final message into view | |
| 1249 | + now, but because viewMessages is currently hidden behind | |
| 1250 | + viewConfig, scrolling is a no-op. */ | |
| 1251 | + Chat.scrollMessagesTo(1); | |
| 1252 | + }else{ | |
| 1253 | + Chat.updateActiveUserList(); | |
| 1254 | + Chat.animate(Chat.e.activeUserListWrapper, 'anim-flip-v'); | |
| 1255 | + } | |
| 1256 | + } | |
| 1257 | + } | |
| 1258 | + }; | |
| 1259 | + if(1){ | |
| 1260 | + /* Per user request, toggle the list of users on and off if the | |
| 1261 | + legend element is tapped. */ | |
| 1262 | + const optAu = namedOptions.activeUsers; | |
| 1263 | + optAu.theLegend = Chat.e.activeUserListWrapper.firstElementChild/*LEGEND*/; | |
| 1264 | + optAu.theList = optAu.theLegend.nextElementSibling/*user list container*/; | |
| 1265 | + optAu.theLegend.addEventListener('click',function(){ | |
| 1266 | + D.toggleClass(Chat.e.activeUserListWrapper, 'collapsed'); | |
| 1267 | + if(!Chat.e.activeUserListWrapper.classList.contains('collapsed')){ | |
| 1268 | + Chat.animate(optAu.theList,'anim-flip-v'); | |
| 1269 | + } | |
| 1270 | + }, false); | |
| 1271 | + }/*namedOptions.activeUsers additional setup*/ | |
| 1272 | + /* Settings menu entries... Remember that they will be rendered in | |
| 1273 | + reverse order and the most frequently-needed ones "should" | |
| 1274 | + (arguably) be closer to the start of this list so that they | |
| 1275 | + will be rendered within easier reach of the settings button. */ | |
| 1050 | 1276 | const settingsOps = [{ |
| 1051 | 1277 | label: "Multi-line input", |
| 1052 | 1278 | boolValue: ()=>Chat.inputElement()===Chat.e.inputMulti, |
| 1053 | 1279 | persistentSetting: 'edit-multiline', |
| 1054 | 1280 | callback: function(){ |
| 1055 | 1281 | Chat.inputToggleSingleMulti(); |
| 1056 | 1282 | } |
| 1057 | 1283 | },{ |
| 1284 | + label: "Left-align my posts", | |
| 1285 | + boolValue: ()=>!document.body.classList.contains('my-messages-right'), | |
| 1286 | + callback: function f(){ | |
| 1287 | + document.body.classList.toggle('my-messages-right'); | |
| 1288 | + } | |
| 1289 | + },{ | |
| 1290 | + label: "Show images inline", | |
| 1291 | + boolValue: ()=>Chat.settings.getBool('images-inline'), | |
| 1292 | + callback: function(){ | |
| 1293 | + const v = Chat.settings.toggle('images-inline'); | |
| 1294 | + F.toast.message("Image mode set to "+(v ? "inline" : "hyperlink")+"."); | |
| 1295 | + } | |
| 1296 | + },{ | |
| 1297 | + label: "Timestamps in active users list", | |
| 1298 | + boolValue: ()=>Chat.e.activeUserList.classList.contains('timestamps'), | |
| 1299 | + persistentSetting: 'active-user-list-timestamps', | |
| 1300 | + callback: function(){ | |
| 1301 | + D.toggleClass(Chat.e.activeUserList,'timestamps'); | |
| 1302 | + /* If the timestamp option is activated but | |
| 1303 | + namedOptions.activeUsers is not currently checked then | |
| 1304 | + toggle that option on as well. */ | |
| 1305 | + if(Chat.e.activeUserList.classList.contains('timestamps') | |
| 1306 | + && !namedOptions.activeUsers.boolValue()){ | |
| 1307 | + namedOptions.activeUsers.checkbox.checked = true; | |
| 1308 | + namedOptions.activeUsers.callback(); | |
| 1309 | + Chat.settings.set(namedOptions.activeUsers.persistentSetting, true); | |
| 1310 | + } | |
| 1311 | + } | |
| 1312 | + }, | |
| 1313 | + namedOptions.activeUsers,{ | |
| 1058 | 1314 | label: "Monospace message font", |
| 1059 | 1315 | boolValue: ()=>document.body.classList.contains('monospace-messages'), |
| 1060 | 1316 | persistentSetting: 'monospace-messages', |
| 1061 | 1317 | callback: function(){ |
| 1062 | 1318 | document.body.classList.toggle('monospace-messages'); |
| @@ -1066,57 +1322,44 @@ | ||
| 1066 | 1322 | boolValue: ()=>Chat.isChatOnlyMode(), |
| 1067 | 1323 | persistentSetting: 'chat-only-mode', |
| 1068 | 1324 | callback: function(){ |
| 1069 | 1325 | Chat.toggleChatOnlyMode(); |
| 1070 | 1326 | } |
| 1071 | - },{ | |
| 1072 | - label: "Left-align my posts", | |
| 1073 | - boolValue: ()=>!document.body.classList.contains('my-messages-right'), | |
| 1074 | - callback: function f(){ | |
| 1075 | - document.body.classList.toggle('my-messages-right'); | |
| 1076 | - } | |
| 1077 | - },{ | |
| 1078 | - label: "Images inline", | |
| 1079 | - boolValue: ()=>Chat.settings.getBool('images-inline'), | |
| 1080 | - callback: function(){ | |
| 1081 | - const v = Chat.settings.toggle('images-inline'); | |
| 1082 | - F.toast.message("Image mode set to "+(v ? "inline" : "hyperlink")+"."); | |
| 1083 | - } | |
| 1084 | 1327 | }]; |
| 1085 | 1328 | |
| 1086 | 1329 | /** Set up selection list of notification sounds. */ |
| 1087 | 1330 | if(1){ |
| 1088 | - settingsOps.selectSound = D.addClass(D.div(), 'menu-entry'); | |
| 1089 | 1331 | const selectSound = D.select(); |
| 1090 | - D.append(settingsOps.selectSound, | |
| 1091 | - D.append(D.span(),"Audio alert"), | |
| 1092 | - selectSound); | |
| 1093 | 1332 | D.option(selectSound, "", "(no audio)"); |
| 1094 | 1333 | const firstSoundIndex = selectSound.options.length; |
| 1095 | - F.config.chat.alerts.forEach(function(a){ | |
| 1096 | - D.option(selectSound, a); | |
| 1097 | - }); | |
| 1334 | + F.config.chat.alerts.forEach((a)=>D.option(selectSound, a)); | |
| 1098 | 1335 | if(true===Chat.settings.getBool('audible-alert')){ |
| 1336 | + /* This setting used to be a plain bool. If we encounter | |
| 1337 | + such a setting, take the first sound in the list. */ | |
| 1099 | 1338 | selectSound.selectedIndex = firstSoundIndex; |
| 1100 | 1339 | }else{ |
| 1101 | 1340 | selectSound.value = Chat.settings.get('audible-alert',''); |
| 1102 | 1341 | if(selectSound.selectedIndex<0){ |
| 1103 | - /*Missing file - removed after this setting was applied. Fall back | |
| 1104 | - to the first sound in the list. */ | |
| 1342 | + /* Missing file - removed after this setting was | |
| 1343 | + applied. Fall back to the first sound in the list. */ | |
| 1105 | 1344 | selectSound.selectedIndex = firstSoundIndex; |
| 1106 | 1345 | } |
| 1107 | 1346 | } |
| 1108 | - selectSound.addEventListener('change',function(){ | |
| 1109 | - const v = this.value; | |
| 1110 | - Chat.setNewMessageSound(v); | |
| 1111 | - F.toast.message("Audio notifications "+(v ? "enabled" : "disabled")+"."); | |
| 1112 | - if(v) setTimeout(()=>Chat.playNewMessageSound(), 0); | |
| 1113 | - }, false); | |
| 1114 | 1347 | Chat.setNewMessageSound(selectSound.value); |
| 1348 | + settingsOps.push({ | |
| 1349 | + label: "Audio alert", | |
| 1350 | + select: selectSound, | |
| 1351 | + callback: function(ev){ | |
| 1352 | + const v = ev.target.value; | |
| 1353 | + Chat.setNewMessageSound(v); | |
| 1354 | + F.toast.message("Audio notifications "+(v ? "enabled" : "disabled")+"."); | |
| 1355 | + if(v) setTimeout(()=>Chat.playNewMessageSound(), 0); | |
| 1356 | + } | |
| 1357 | + }); | |
| 1115 | 1358 | }/*audio notification config*/ |
| 1116 | 1359 | /** |
| 1117 | - Build list of options... | |
| 1360 | + Build UI for config options... | |
| 1118 | 1361 | */ |
| 1119 | 1362 | settingsOps.forEach(function f(op){ |
| 1120 | 1363 | const line = D.addClass(D.div(), 'menu-entry'); |
| 1121 | 1364 | const btn = D.append( |
| 1122 | 1365 | D.addClass(D.label(), 'cbutton'/*bootstrap skin hijacks 'button'*/), |
| @@ -1125,62 +1368,52 @@ | ||
| 1125 | 1368 | op.callback(ev); |
| 1126 | 1369 | if(op.persistentSetting){ |
| 1127 | 1370 | Chat.settings.set(op.persistentSetting, op.boolValue()); |
| 1128 | 1371 | } |
| 1129 | 1372 | }; |
| 1130 | - if(op.hasOwnProperty('boolValue')){ | |
| 1373 | + if(op.hasOwnProperty('select')){ | |
| 1374 | + D.append(line, btn, op.select); | |
| 1375 | + op.select.addEventListener('change', callback, false); | |
| 1376 | + }else if(op.hasOwnProperty('boolValue')){ | |
| 1131 | 1377 | if(undefined === f.$id) f.$id = 0; |
| 1132 | 1378 | ++f.$id; |
| 1133 | - const check = D.attr(D.checkbox(1, op.boolValue()), | |
| 1134 | - 'aria-label', op.label); | |
| 1379 | + const check = op.checkbox | |
| 1380 | + = D.attr(D.checkbox(1, op.boolValue()), | |
| 1381 | + 'aria-label', op.label); | |
| 1135 | 1382 | const id = 'cfgopt'+f.$id; |
| 1136 | 1383 | if(op.boolValue()) check.checked = true; |
| 1137 | 1384 | D.attr(check, 'id', id); |
| 1138 | 1385 | D.attr(btn, 'for', id); |
| 1139 | 1386 | D.append(line, check); |
| 1140 | 1387 | check.addEventListener('change', callback); |
| 1388 | + D.append(line, btn); | |
| 1141 | 1389 | }else{ |
| 1142 | 1390 | line.addEventListener('click', callback); |
| 1391 | + D.append(line, btn); | |
| 1143 | 1392 | } |
| 1144 | - D.append(line, btn); | |
| 1145 | 1393 | D.append(optionsMenu, line); |
| 1146 | 1394 | }); |
| 1147 | - if(settingsOps.selectSound){ | |
| 1395 | + if(0 && settingsOps.selectSound){ | |
| 1148 | 1396 | D.append(optionsMenu, settingsOps.selectSound); |
| 1149 | 1397 | } |
| 1150 | 1398 | //settingsButton.click()/*for for development*/; |
| 1151 | 1399 | })()/*#chat-settings-button setup*/; |
| 1152 | 1400 | |
| 1153 | 1401 | (function(){/*set up message preview*/ |
| 1154 | 1402 | const btnPreview = Chat.e.btnPreview; |
| 1155 | 1403 | Chat.setPreviewText = function(t){ |
| 1156 | - this.revealPreview(true).e.previewContent.innerHTML = t; | |
| 1157 | - this.e.previewArea.querySelectorAll('a').forEach(addAnchorTargetBlank); | |
| 1404 | + this.setCurrentView(this.e.viewPreview); | |
| 1405 | + this.e.previewContent.innerHTML = t; | |
| 1406 | + this.e.viewPreview.querySelectorAll('a').forEach(addAnchorTargetBlank); | |
| 1158 | 1407 | this.e.inputCurrent.focus(); |
| 1159 | 1408 | }; |
| 1160 | - /** | |
| 1161 | - Reveals preview area if showIt is true, else hides it. | |
| 1162 | - This also shows/hides other elements, "as appropriate." | |
| 1163 | - */ | |
| 1164 | - Chat.revealPreview = function(showIt){ | |
| 1165 | - if(showIt){ | |
| 1166 | - D.removeClass(Chat.e.previewArea, 'hidden'); | |
| 1167 | - D.addClass([Chat.e.messagesWrapper, Chat.e.configArea], | |
| 1168 | - 'hidden'); | |
| 1169 | - }else{ | |
| 1170 | - D.addClass([Chat.e.configArea, Chat.e.previewArea], 'hidden'); | |
| 1171 | - D.removeClass(Chat.e.messagesWrapper, 'hidden'); | |
| 1172 | - } | |
| 1173 | - return this; | |
| 1174 | - }; | |
| 1175 | - Chat.e.previewArea.querySelector('#chat-preview-close'). | |
| 1176 | - addEventListener('click', ()=>Chat.revealPreview(false), false); | |
| 1409 | + Chat.e.viewPreview.querySelector('#chat-preview-close'). | |
| 1410 | + addEventListener('click', ()=>Chat.setCurrentView(Chat.e.viewMessages), false); | |
| 1177 | 1411 | let previewPending = false; |
| 1178 | 1412 | const elemsToEnable = [ |
| 1179 | 1413 | btnPreview, Chat.e.btnSubmit, |
| 1180 | 1414 | Chat.e.inputSingle, Chat.e.inputMulti]; |
| 1181 | - Chat.disableDuringAjax.push(btnPreview); | |
| 1182 | 1415 | const submit = function(ev){ |
| 1183 | 1416 | ev.preventDefault(); |
| 1184 | 1417 | ev.stopPropagation(); |
| 1185 | 1418 | if(previewPending) return false; |
| 1186 | 1419 | const txt = Chat.e.inputCurrent.value; |
| @@ -1209,12 +1442,12 @@ | ||
| 1209 | 1442 | previewPending = true; |
| 1210 | 1443 | Chat.setPreviewText("Loading preview..."); |
| 1211 | 1444 | }, |
| 1212 | 1445 | aftersend:function(){ |
| 1213 | 1446 | previewPending = false; |
| 1214 | - D.enable(elemsToEnable); | |
| 1215 | 1447 | Chat.ajaxEnd(); |
| 1448 | + D.enable(elemsToEnable); | |
| 1216 | 1449 | } |
| 1217 | 1450 | }); |
| 1218 | 1451 | return false; |
| 1219 | 1452 | }; |
| 1220 | 1453 | btnPreview.addEventListener('click', submit, false); |
| @@ -1231,10 +1464,18 @@ | ||
| 1231 | 1464 | should only be true when loading older messages. */ |
| 1232 | 1465 | f.processPost = function(m,atEnd){ |
| 1233 | 1466 | ++Chat.totalMessageCount; |
| 1234 | 1467 | if( m.msgid>Chat.mxMsg ) Chat.mxMsg = m.msgid; |
| 1235 | 1468 | if( !Chat.mnMsg || m.msgid<Chat.mnMsg) Chat.mnMsg = m.msgid; |
| 1469 | + if(m.xfrom && m.mtime){ | |
| 1470 | + const d = new Date(m.mtime); | |
| 1471 | + const uls = Chat.usersLastSeen[m.xfrom]; | |
| 1472 | + if(!uls || uls<d){ | |
| 1473 | + d.$uColor = m.uclr; | |
| 1474 | + Chat.usersLastSeen[m.xfrom] = d; | |
| 1475 | + } | |
| 1476 | + } | |
| 1236 | 1477 | if( m.mdel ){ |
| 1237 | 1478 | /* A record deletion notice. */ |
| 1238 | 1479 | Chat.deleteMessageElem(m.mdel); |
| 1239 | 1480 | return; |
| 1240 | 1481 | } |
| @@ -1247,10 +1488,11 @@ | ||
| 1247 | 1488 | Chat._gotServerError = m; |
| 1248 | 1489 | } |
| 1249 | 1490 | }/*processPost()*/; |
| 1250 | 1491 | }/*end static init*/ |
| 1251 | 1492 | jx.msgs.forEach((m)=>f.processPost(m,atEnd)); |
| 1493 | + Chat.updateActiveUserList(); | |
| 1252 | 1494 | if('visible'===document.visibilityState){ |
| 1253 | 1495 | if(Chat.changesSincePageHidden){ |
| 1254 | 1496 | Chat.changesSincePageHidden = 0; |
| 1255 | 1497 | Chat.e.pageTitle.innerText = Chat.pageTitleOrig; |
| 1256 | 1498 | } |
| @@ -1273,14 +1515,14 @@ | ||
| 1273 | 1515 | D.fieldset(loadLegend), "id", "load-msg-toolbar" |
| 1274 | 1516 | ); |
| 1275 | 1517 | Chat.disableDuringAjax.push(toolbar); |
| 1276 | 1518 | /* Loads the next n oldest messages, or all previous history if n is negative. */ |
| 1277 | 1519 | const loadOldMessages = function(n){ |
| 1278 | - Chat.e.messagesWrapper.classList.add('loading'); | |
| 1520 | + Chat.e.viewMessages.classList.add('loading'); | |
| 1279 | 1521 | Chat._isBatchLoading = true; |
| 1280 | - const scrollHt = Chat.e.messagesWrapper.scrollHeight, | |
| 1281 | - scrollTop = Chat.e.messagesWrapper.scrollTop; | |
| 1522 | + const scrollHt = Chat.e.viewMessages.scrollHeight, | |
| 1523 | + scrollTop = Chat.e.viewMessages.scrollTop; | |
| 1282 | 1524 | F.fetch("chat-poll",{ |
| 1283 | 1525 | urlParams:{ |
| 1284 | 1526 | before: Chat.mnMsg, |
| 1285 | 1527 | n: n |
| 1286 | 1528 | }, |
| @@ -1291,10 +1533,11 @@ | ||
| 1291 | 1533 | }, |
| 1292 | 1534 | onload:function(x){ |
| 1293 | 1535 | let gotMessages = x.msgs.length; |
| 1294 | 1536 | newcontent(x,true); |
| 1295 | 1537 | Chat._isBatchLoading = false; |
| 1538 | + Chat.updateActiveUserList(); | |
| 1296 | 1539 | if(Chat._gotServerError){ |
| 1297 | 1540 | Chat._gotServerError = false; |
| 1298 | 1541 | return; |
| 1299 | 1542 | } |
| 1300 | 1543 | if(n<0/*we asked for all history*/ |
| @@ -1314,17 +1557,17 @@ | ||
| 1314 | 1557 | } |
| 1315 | 1558 | if(gotMessages > 0){ |
| 1316 | 1559 | F.toast.message("Loaded "+gotMessages+" older messages."); |
| 1317 | 1560 | /* Return scroll position to where it was when the history load |
| 1318 | 1561 | was requested, per user request */ |
| 1319 | - Chat.e.messagesWrapper.scrollTo( | |
| 1320 | - 0, Chat.e.messagesWrapper.scrollHeight - scrollHt + scrollTop | |
| 1562 | + Chat.e.viewMessages.scrollTo( | |
| 1563 | + 0, Chat.e.viewMessages.scrollHeight - scrollHt + scrollTop | |
| 1321 | 1564 | ); |
| 1322 | 1565 | } |
| 1323 | 1566 | }, |
| 1324 | 1567 | aftersend:function(){ |
| 1325 | - Chat.e.messagesWrapper.classList.remove('loading'); | |
| 1568 | + Chat.e.viewMessages.classList.remove('loading'); | |
| 1326 | 1569 | Chat.ajaxEnd(); |
| 1327 | 1570 | } |
| 1328 | 1571 | }); |
| 1329 | 1572 | }; |
| 1330 | 1573 | const wrapper = D.div(); /* browsers don't all properly handle >1 child in a fieldset */; |
| @@ -1333,19 +1576,19 @@ | ||
| 1333 | 1576 | D.append(wrapper, btn); |
| 1334 | 1577 | btn.addEventListener('click',()=>loadOldMessages(Chat.loadMessageCount)); |
| 1335 | 1578 | btn = D.button("All previous messages"); |
| 1336 | 1579 | D.append(wrapper, btn); |
| 1337 | 1580 | btn.addEventListener('click',()=>loadOldMessages(-1)); |
| 1338 | - D.append(Chat.e.messagesWrapper, toolbar); | |
| 1581 | + D.append(Chat.e.viewMessages, toolbar); | |
| 1339 | 1582 | toolbar.disabled = true /*will be enabled when msg load finishes */; |
| 1340 | 1583 | })()/*end history loading widget setup*/; |
| 1341 | 1584 | |
| 1342 | 1585 | const afterFetch = function f(){ |
| 1343 | 1586 | if(true===f.isFirstCall){ |
| 1344 | 1587 | f.isFirstCall = false; |
| 1345 | 1588 | Chat.ajaxEnd(); |
| 1346 | - Chat.e.messagesWrapper.classList.remove('loading'); | |
| 1589 | + Chat.e.viewMessages.classList.remove('loading'); | |
| 1347 | 1590 | setTimeout(function(){ |
| 1348 | 1591 | Chat.scrollMessagesTo(1); |
| 1349 | 1592 | }, 250); |
| 1350 | 1593 | } |
| 1351 | 1594 | if(Chat._gotServerError && Chat.intervalTimer){ |
| @@ -1363,11 +1606,11 @@ | ||
| 1363 | 1606 | f.running = true; |
| 1364 | 1607 | Chat._isBatchLoading = f.isFirstCall; |
| 1365 | 1608 | if(true===f.isFirstCall){ |
| 1366 | 1609 | f.isFirstCall = false; |
| 1367 | 1610 | Chat.ajaxStart(); |
| 1368 | - Chat.e.messagesWrapper.classList.add('loading'); | |
| 1611 | + Chat.e.viewMessages.classList.add('loading'); | |
| 1369 | 1612 | } |
| 1370 | 1613 | F.fetch("chat-poll",{ |
| 1371 | 1614 | timeout: 420 * 1000/*FIXME: get the value from the server*/, |
| 1372 | 1615 | urlParams:{ |
| 1373 | 1616 | name: Chat.mxMsg |
| @@ -1384,11 +1627,14 @@ | ||
| 1384 | 1627 | resumed, and reportError() produces a loud error message. */ |
| 1385 | 1628 | afterFetch(); |
| 1386 | 1629 | }, |
| 1387 | 1630 | onload:function(y){ |
| 1388 | 1631 | newcontent(y); |
| 1389 | - Chat._isBatchLoading = false; | |
| 1632 | + if(Chat._isBatchLoading){ | |
| 1633 | + Chat._isBatchLoading = false; | |
| 1634 | + Chat.updateActiveUserList(); | |
| 1635 | + } | |
| 1390 | 1636 | afterFetch(); |
| 1391 | 1637 | } |
| 1392 | 1638 | }); |
| 1393 | 1639 | }; |
| 1394 | 1640 | poll.isFirstCall = true; |
| @@ -1395,7 +1641,15 @@ | ||
| 1395 | 1641 | Chat._gotServerError = poll.running = false; |
| 1396 | 1642 | if( window.fossil.config.chat.fromcli ){ |
| 1397 | 1643 | Chat.chatOnlyMode(true); |
| 1398 | 1644 | } |
| 1399 | 1645 | Chat.intervalTimer = setInterval(poll, 1000); |
| 1646 | + if(0){ | |
| 1647 | + const flip = (ev)=>Chat.animate(ev.target,'anim-flip-h'); | |
| 1648 | + document.querySelectorAll('#chat-edit-buttons button').forEach(function(e){ | |
| 1649 | + e.addEventListener('click',flip, false); | |
| 1650 | + }); | |
| 1651 | + } | |
| 1652 | + setTimeout( ()=>Chat.inputFocus(), 0 ); | |
| 1653 | + Chat.animate.$disabled = false; | |
| 1400 | 1654 | F.page.chat = Chat/* enables testing the APIs via the dev tools */; |
| 1401 | 1655 | })(); |
| 1402 | 1656 |
| --- src/chat.js | |
| +++ src/chat.js | |
| @@ -34,10 +34,30 @@ | |
| 34 | else if(r1.bottom<=r2.bottom && r1.bottom>=r2.top) return true; |
| 35 | return false; |
| 36 | }; |
| 37 | |
| 38 | const addAnchorTargetBlank = (e)=>D.attr(e, 'target','_blank'); |
| 39 | |
| 40 | (function(){ |
| 41 | let dbg = document.querySelector('#debugMsg'); |
| 42 | if(dbg){ |
| 43 | /* This can inadvertently influence our flexbox layouts, so move |
| @@ -74,11 +94,11 @@ | |
| 74 | ht = wh - extra; |
| 75 | } |
| 76 | f.contentArea.style.height = |
| 77 | f.contentArea.style.maxHeight = [ |
| 78 | "calc(", (ht>=100 ? ht : 100), "px", |
| 79 | " - 1em"/*fudge value*/,")" |
| 80 | /* ^^^^ hypothetically not needed, but both Chrome/FF on |
| 81 | Linux will force scrollbars on the body if this value is |
| 82 | too small (<0.75em in my tests). */ |
| 83 | ].join(''); |
| 84 | if(false){ |
| @@ -105,21 +125,24 @@ | |
| 105 | pageTitle: E1('head title'), |
| 106 | loadOlderToolbar: undefined /* the load-posts toolbar (dynamically created) */, |
| 107 | inputWrapper: E1("#chat-input-area"), |
| 108 | inputLine: E1('#chat-input-line'), |
| 109 | fileSelectWrapper: E1('#chat-input-file-area'), |
| 110 | messagesWrapper: E1('#chat-messages-wrapper'), |
| 111 | btnSubmit: E1('#chat-message-submit'), |
| 112 | inputSingle: E1('#chat-input-single'), |
| 113 | inputMulti: E1('#chat-input-multi'), |
| 114 | inputCurrent: undefined/*one of inputSingle or inputMulti*/, |
| 115 | inputFile: E1('#chat-input-file'), |
| 116 | contentDiv: E1('div.content'), |
| 117 | configArea: E1('#chat-config'), |
| 118 | previewArea: E1('#chat-preview'), |
| 119 | previewContent: E1('#chat-preview-content'), |
| 120 | btnPreview: E1('#chat-preview-button') |
| 121 | }, |
| 122 | me: F.user.name, |
| 123 | mxMsg: F.config.chat.initSize ? -F.config.chat.initSize : -50, |
| 124 | mnMsg: undefined/*lowest message ID we've seen so far (for history loading)*/, |
| 125 | pageIsActive: 'visible'===document.visibilityState, |
| @@ -127,10 +150,23 @@ | |
| 127 | notificationBubbleColor: 'white', |
| 128 | totalMessageCount: 0, // total # of inbound messages |
| 129 | //! Number of messages to load for the history buttons |
| 130 | loadMessageCount: Math.abs(F.config.chat.initSize || 20), |
| 131 | ajaxInflight: 0, |
| 132 | /** Gets (no args) or sets (1 arg) the current input text field value, |
| 133 | taking into account single- vs multi-line input. The getter returns |
| 134 | a string and the setter returns this object. */ |
| 135 | inputValue: function(){ |
| 136 | const e = this.inputElement(); |
| @@ -157,19 +193,20 @@ | |
| 157 | this.e.inputLine.classList.remove('single-line'); |
| 158 | }else{ |
| 159 | this.e.inputCurrent = this.e.inputSingle; |
| 160 | this.e.inputLine.classList.add('single-line'); |
| 161 | } |
| 162 | const m = this.e.messagesWrapper, |
| 163 | sTop = m.scrollTop, |
| 164 | mh1 = m.clientHeight; |
| 165 | D.addClass(old, 'hidden'); |
| 166 | D.removeClass(this.e.inputCurrent, 'hidden'); |
| 167 | const mh2 = m.clientHeight; |
| 168 | m.scrollTo(0, sTop + (mh1-mh2)); |
| 169 | this.e.inputCurrent.value = old.value; |
| 170 | old.value = ''; |
| 171 | return this; |
| 172 | }, |
| 173 | /** |
| 174 | If passed true or no arguments, switches to multi-line mode |
| 175 | if currently in single-line mode. If passed false, switches |
| @@ -241,12 +278,15 @@ | |
| 241 | /* Injects DOM element e as a new row in the chat, at the oldest |
| 242 | end of the list if atEnd is truthy, else at the newest end of |
| 243 | the list. */ |
| 244 | injectMessageElem: function f(e, atEnd){ |
| 245 | const mip = atEnd ? this.e.loadOlderToolbar : this.e.messageInjectPoint, |
| 246 | holder = this.e.messagesWrapper, |
| 247 | prevMessage = this.e.newestMessage; |
| 248 | if(atEnd){ |
| 249 | const fe = mip.nextElementSibling; |
| 250 | if(fe) mip.parentNode.insertBefore(e, fe); |
| 251 | else D.append(mip.parentNode, e); |
| 252 | }else{ |
| @@ -337,22 +377,22 @@ | |
| 337 | <0 = top of the message list, >0 = bottom of the message list, |
| 338 | 0 == the newest message (normally the same position as >1). |
| 339 | */ |
| 340 | scrollMessagesTo: function(where){ |
| 341 | if(where<0){ |
| 342 | Chat.e.messagesWrapper.scrollTop = 0; |
| 343 | }else if(where>0){ |
| 344 | Chat.e.messagesWrapper.scrollTop = Chat.e.messagesWrapper.scrollHeight; |
| 345 | }else if(Chat.e.newestMessage){ |
| 346 | Chat.e.newestMessage.scrollIntoView(false); |
| 347 | } |
| 348 | }, |
| 349 | toggleChatOnlyMode: function(){ |
| 350 | return this.chatOnlyMode(!this.isChatOnlyMode()); |
| 351 | }, |
| 352 | messageIsInView: function(e){ |
| 353 | return e ? overlapsElemView(e, this.e.messagesWrapper) : false; |
| 354 | }, |
| 355 | settings:{ |
| 356 | get: (k,dflt)=>F.storage.get(k,dflt), |
| 357 | getBool: (k,dflt)=>F.storage.getBool(k,dflt), |
| 358 | set: (k,v)=>F.storage.set(k,v), |
| @@ -366,11 +406,13 @@ | |
| 366 | defaults:{ |
| 367 | "images-inline": !!F.config.chat.imagesInline, |
| 368 | "edit-multiline": false, |
| 369 | "monospace-messages": false, |
| 370 | "chat-only-mode": false, |
| 371 | "audible-alert": true |
| 372 | } |
| 373 | }, |
| 374 | /** Plays a new-message notification sound IF the audible-alert |
| 375 | setting is true, else this is a no-op. Returns this. |
| 376 | */ |
| @@ -395,12 +437,117 @@ | |
| 395 | setNewMessageSound: function f(uri){ |
| 396 | delete this.playNewMessageSound.audio; |
| 397 | this.playNewMessageSound.uri = uri; |
| 398 | this.settings.set('audible-alert', uri); |
| 399 | return this; |
| 400 | } |
| 401 | }; |
| 402 | F.fetch.beforesend = ()=>cs.ajaxStart(); |
| 403 | F.fetch.aftersend = ()=>cs.ajaxEnd(); |
| 404 | cs.e.inputCurrent = cs.e.inputSingle; |
| 405 | /* Install default settings... */ |
| 406 | Object.keys(cs.settings.defaults).forEach(function(k){ |
| @@ -415,10 +562,16 @@ | |
| 415 | tall vs wide. Can be toggled via settings popup. */ |
| 416 | document.body.classList.add('my-messages-right'); |
| 417 | } |
| 418 | if(cs.settings.getBool('monospace-messages',false)){ |
| 419 | document.body.classList.add('monospace-messages'); |
| 420 | } |
| 421 | cs.inputMultilineMode(cs.settings.getBool('edit-multiline',false)); |
| 422 | cs.chatOnlyMode(cs.settings.getBool('chat-only-mode')); |
| 423 | cs.pageTitleOrig = cs.e.pageTitle.innerText; |
| 424 | const qs = (e)=>document.querySelector(e); |
| @@ -611,10 +764,37 @@ | |
| 611 | cs.pageIsActive = ('visible' === document.visibilityState); |
| 612 | if(cs.pageIsActive){ |
| 613 | cs.e.pageTitle.innerText = cs.pageTitleOrig; |
| 614 | } |
| 615 | }, true); |
| 616 | return cs; |
| 617 | })()/*Chat initialization*/; |
| 618 | |
| 619 | /** |
| 620 | Custom widget type for rendering messages (one message per |
| @@ -658,21 +838,10 @@ | |
| 658 | d.getHours(),":", |
| 659 | (d.getMinutes()+100).toString().slice(1,3), |
| 660 | ' ', dowMap[d.getDay()] |
| 661 | ].join(''); |
| 662 | }; |
| 663 | /** Returns the local time string of Date object d, defaulting |
| 664 | to the current time. */ |
| 665 | const localTimeString = function ff(d){ |
| 666 | d || (d = new Date()); |
| 667 | return [ |
| 668 | d.getFullYear(),'-',pad2(d.getMonth()+1/*sigh*/), |
| 669 | '-',pad2(d.getDate()), |
| 670 | ' ',pad2(d.getHours()),':',pad2(d.getMinutes()), |
| 671 | ':',pad2(d.getSeconds()) |
| 672 | ].join(''); |
| 673 | }; |
| 674 | cf.prototype = { |
| 675 | scrollIntoView: function(){ |
| 676 | this.e.content.scrollIntoView(); |
| 677 | }, |
| 678 | setMessage: function(m){ |
| @@ -758,14 +927,14 @@ | |
| 758 | eXFrom.addEventListener('click', ()=>this.e.tab.click(), false); |
| 759 | }*/ |
| 760 | return this; |
| 761 | }, |
| 762 | /* Event handler for clicking .message-user elements to show their |
| 763 | timestamps. */ |
| 764 | _handleLegendClicked: function f(ev){ |
| 765 | if(!f.popup){ |
| 766 | /* Timestamp popup widget */ |
| 767 | f.popup = { |
| 768 | e: D.addClass(D.div(), 'chat-message-popup'), |
| 769 | refresh:function(){ |
| 770 | const eMsg = this.$eMsg/*.message-widget element*/; |
| 771 | if(!eMsg) return; |
| @@ -830,18 +999,45 @@ | |
| 830 | y: 'a' |
| 831 | }), "User's Timeline"), |
| 832 | 'target', '_blank' |
| 833 | ); |
| 834 | D.append(toolbar2, timelineLink); |
| 835 | } |
| 836 | const tab = eMsg.querySelector('.message-widget-tab'); |
| 837 | D.append(tab, this.e); |
| 838 | D.removeClass(this.e, 'hidden'); |
| 839 | }/*refresh()*/, |
| 840 | hide: function(){ |
| 841 | D.addClass(D.clearElement(this.e), 'hidden'); |
| 842 | delete this.$eMsg; |
| 843 | }, |
| 844 | show: function(tgtMsg){ |
| 845 | if(tgtMsg === this.$eMsg){ |
| 846 | this.hide(); |
| 847 | return; |
| @@ -961,11 +1157,11 @@ | |
| 961 | */ |
| 962 | Chat.submitMessage = function f(){ |
| 963 | if(!f.spaces){ |
| 964 | f.spaces = /\s+$/; |
| 965 | } |
| 966 | this.revealPreview(false); |
| 967 | const fd = new FormData(); |
| 968 | var msg = this.inputValue().trim(); |
| 969 | if(msg && (msg.indexOf('\n')>0 || f.spaces.test(msg))){ |
| 970 | /* Cosmetic: trim whitespace from the ends of lines to try to |
| 971 | keep copy/paste from terminals, especially wide ones, from |
| @@ -1021,42 +1217,102 @@ | |
| 1021 | e.preventDefault(); |
| 1022 | Chat.submitMessage(); |
| 1023 | return false; |
| 1024 | }); |
| 1025 | |
| 1026 | /* Returns an almost-ISO8601 form of Date object d. */ |
| 1027 | const iso8601ish = function(d){ |
| 1028 | return d.toISOString() |
| 1029 | .replace('T',' ').replace(/\.\d+/,'').replace('Z', ' zulu'); |
| 1030 | }; |
| 1031 | |
| 1032 | (function(){/*Set up #chat-settings-button */ |
| 1033 | const settingsButton = document.querySelector('#chat-settings-button'); |
| 1034 | const optionsMenu = E1('#chat-config-options'); |
| 1035 | const cbToggle = function(ev){ |
| 1036 | ev.preventDefault(); |
| 1037 | ev.stopPropagation(); |
| 1038 | if(Chat.e.configArea.classList.contains('hidden')){ |
| 1039 | D.removeClass(Chat.e.configArea, 'hidden'); |
| 1040 | D.addClass([Chat.e.messagesWrapper, Chat.e.previewArea], 'hidden'); |
| 1041 | }else{ |
| 1042 | D.addClass(Chat.e.configArea, 'hidden'); |
| 1043 | D.removeClass(Chat.e.messagesWrapper, 'hidden'); |
| 1044 | } |
| 1045 | return false; |
| 1046 | }; |
| 1047 | D.attr(settingsButton, 'role', 'button').addEventListener('click', cbToggle, false); |
| 1048 | Chat.e.configArea.querySelector('button').addEventListener('click', cbToggle, false); |
| 1049 | /* Settings menu entries... */ |
| 1050 | const settingsOps = [{ |
| 1051 | label: "Multi-line input", |
| 1052 | boolValue: ()=>Chat.inputElement()===Chat.e.inputMulti, |
| 1053 | persistentSetting: 'edit-multiline', |
| 1054 | callback: function(){ |
| 1055 | Chat.inputToggleSingleMulti(); |
| 1056 | } |
| 1057 | },{ |
| 1058 | label: "Monospace message font", |
| 1059 | boolValue: ()=>document.body.classList.contains('monospace-messages'), |
| 1060 | persistentSetting: 'monospace-messages', |
| 1061 | callback: function(){ |
| 1062 | document.body.classList.toggle('monospace-messages'); |
| @@ -1066,57 +1322,44 @@ | |
| 1066 | boolValue: ()=>Chat.isChatOnlyMode(), |
| 1067 | persistentSetting: 'chat-only-mode', |
| 1068 | callback: function(){ |
| 1069 | Chat.toggleChatOnlyMode(); |
| 1070 | } |
| 1071 | },{ |
| 1072 | label: "Left-align my posts", |
| 1073 | boolValue: ()=>!document.body.classList.contains('my-messages-right'), |
| 1074 | callback: function f(){ |
| 1075 | document.body.classList.toggle('my-messages-right'); |
| 1076 | } |
| 1077 | },{ |
| 1078 | label: "Images inline", |
| 1079 | boolValue: ()=>Chat.settings.getBool('images-inline'), |
| 1080 | callback: function(){ |
| 1081 | const v = Chat.settings.toggle('images-inline'); |
| 1082 | F.toast.message("Image mode set to "+(v ? "inline" : "hyperlink")+"."); |
| 1083 | } |
| 1084 | }]; |
| 1085 | |
| 1086 | /** Set up selection list of notification sounds. */ |
| 1087 | if(1){ |
| 1088 | settingsOps.selectSound = D.addClass(D.div(), 'menu-entry'); |
| 1089 | const selectSound = D.select(); |
| 1090 | D.append(settingsOps.selectSound, |
| 1091 | D.append(D.span(),"Audio alert"), |
| 1092 | selectSound); |
| 1093 | D.option(selectSound, "", "(no audio)"); |
| 1094 | const firstSoundIndex = selectSound.options.length; |
| 1095 | F.config.chat.alerts.forEach(function(a){ |
| 1096 | D.option(selectSound, a); |
| 1097 | }); |
| 1098 | if(true===Chat.settings.getBool('audible-alert')){ |
| 1099 | selectSound.selectedIndex = firstSoundIndex; |
| 1100 | }else{ |
| 1101 | selectSound.value = Chat.settings.get('audible-alert',''); |
| 1102 | if(selectSound.selectedIndex<0){ |
| 1103 | /*Missing file - removed after this setting was applied. Fall back |
| 1104 | to the first sound in the list. */ |
| 1105 | selectSound.selectedIndex = firstSoundIndex; |
| 1106 | } |
| 1107 | } |
| 1108 | selectSound.addEventListener('change',function(){ |
| 1109 | const v = this.value; |
| 1110 | Chat.setNewMessageSound(v); |
| 1111 | F.toast.message("Audio notifications "+(v ? "enabled" : "disabled")+"."); |
| 1112 | if(v) setTimeout(()=>Chat.playNewMessageSound(), 0); |
| 1113 | }, false); |
| 1114 | Chat.setNewMessageSound(selectSound.value); |
| 1115 | }/*audio notification config*/ |
| 1116 | /** |
| 1117 | Build list of options... |
| 1118 | */ |
| 1119 | settingsOps.forEach(function f(op){ |
| 1120 | const line = D.addClass(D.div(), 'menu-entry'); |
| 1121 | const btn = D.append( |
| 1122 | D.addClass(D.label(), 'cbutton'/*bootstrap skin hijacks 'button'*/), |
| @@ -1125,62 +1368,52 @@ | |
| 1125 | op.callback(ev); |
| 1126 | if(op.persistentSetting){ |
| 1127 | Chat.settings.set(op.persistentSetting, op.boolValue()); |
| 1128 | } |
| 1129 | }; |
| 1130 | if(op.hasOwnProperty('boolValue')){ |
| 1131 | if(undefined === f.$id) f.$id = 0; |
| 1132 | ++f.$id; |
| 1133 | const check = D.attr(D.checkbox(1, op.boolValue()), |
| 1134 | 'aria-label', op.label); |
| 1135 | const id = 'cfgopt'+f.$id; |
| 1136 | if(op.boolValue()) check.checked = true; |
| 1137 | D.attr(check, 'id', id); |
| 1138 | D.attr(btn, 'for', id); |
| 1139 | D.append(line, check); |
| 1140 | check.addEventListener('change', callback); |
| 1141 | }else{ |
| 1142 | line.addEventListener('click', callback); |
| 1143 | } |
| 1144 | D.append(line, btn); |
| 1145 | D.append(optionsMenu, line); |
| 1146 | }); |
| 1147 | if(settingsOps.selectSound){ |
| 1148 | D.append(optionsMenu, settingsOps.selectSound); |
| 1149 | } |
| 1150 | //settingsButton.click()/*for for development*/; |
| 1151 | })()/*#chat-settings-button setup*/; |
| 1152 | |
| 1153 | (function(){/*set up message preview*/ |
| 1154 | const btnPreview = Chat.e.btnPreview; |
| 1155 | Chat.setPreviewText = function(t){ |
| 1156 | this.revealPreview(true).e.previewContent.innerHTML = t; |
| 1157 | this.e.previewArea.querySelectorAll('a').forEach(addAnchorTargetBlank); |
| 1158 | this.e.inputCurrent.focus(); |
| 1159 | }; |
| 1160 | /** |
| 1161 | Reveals preview area if showIt is true, else hides it. |
| 1162 | This also shows/hides other elements, "as appropriate." |
| 1163 | */ |
| 1164 | Chat.revealPreview = function(showIt){ |
| 1165 | if(showIt){ |
| 1166 | D.removeClass(Chat.e.previewArea, 'hidden'); |
| 1167 | D.addClass([Chat.e.messagesWrapper, Chat.e.configArea], |
| 1168 | 'hidden'); |
| 1169 | }else{ |
| 1170 | D.addClass([Chat.e.configArea, Chat.e.previewArea], 'hidden'); |
| 1171 | D.removeClass(Chat.e.messagesWrapper, 'hidden'); |
| 1172 | } |
| 1173 | return this; |
| 1174 | }; |
| 1175 | Chat.e.previewArea.querySelector('#chat-preview-close'). |
| 1176 | addEventListener('click', ()=>Chat.revealPreview(false), false); |
| 1177 | let previewPending = false; |
| 1178 | const elemsToEnable = [ |
| 1179 | btnPreview, Chat.e.btnSubmit, |
| 1180 | Chat.e.inputSingle, Chat.e.inputMulti]; |
| 1181 | Chat.disableDuringAjax.push(btnPreview); |
| 1182 | const submit = function(ev){ |
| 1183 | ev.preventDefault(); |
| 1184 | ev.stopPropagation(); |
| 1185 | if(previewPending) return false; |
| 1186 | const txt = Chat.e.inputCurrent.value; |
| @@ -1209,12 +1442,12 @@ | |
| 1209 | previewPending = true; |
| 1210 | Chat.setPreviewText("Loading preview..."); |
| 1211 | }, |
| 1212 | aftersend:function(){ |
| 1213 | previewPending = false; |
| 1214 | D.enable(elemsToEnable); |
| 1215 | Chat.ajaxEnd(); |
| 1216 | } |
| 1217 | }); |
| 1218 | return false; |
| 1219 | }; |
| 1220 | btnPreview.addEventListener('click', submit, false); |
| @@ -1231,10 +1464,18 @@ | |
| 1231 | should only be true when loading older messages. */ |
| 1232 | f.processPost = function(m,atEnd){ |
| 1233 | ++Chat.totalMessageCount; |
| 1234 | if( m.msgid>Chat.mxMsg ) Chat.mxMsg = m.msgid; |
| 1235 | if( !Chat.mnMsg || m.msgid<Chat.mnMsg) Chat.mnMsg = m.msgid; |
| 1236 | if( m.mdel ){ |
| 1237 | /* A record deletion notice. */ |
| 1238 | Chat.deleteMessageElem(m.mdel); |
| 1239 | return; |
| 1240 | } |
| @@ -1247,10 +1488,11 @@ | |
| 1247 | Chat._gotServerError = m; |
| 1248 | } |
| 1249 | }/*processPost()*/; |
| 1250 | }/*end static init*/ |
| 1251 | jx.msgs.forEach((m)=>f.processPost(m,atEnd)); |
| 1252 | if('visible'===document.visibilityState){ |
| 1253 | if(Chat.changesSincePageHidden){ |
| 1254 | Chat.changesSincePageHidden = 0; |
| 1255 | Chat.e.pageTitle.innerText = Chat.pageTitleOrig; |
| 1256 | } |
| @@ -1273,14 +1515,14 @@ | |
| 1273 | D.fieldset(loadLegend), "id", "load-msg-toolbar" |
| 1274 | ); |
| 1275 | Chat.disableDuringAjax.push(toolbar); |
| 1276 | /* Loads the next n oldest messages, or all previous history if n is negative. */ |
| 1277 | const loadOldMessages = function(n){ |
| 1278 | Chat.e.messagesWrapper.classList.add('loading'); |
| 1279 | Chat._isBatchLoading = true; |
| 1280 | const scrollHt = Chat.e.messagesWrapper.scrollHeight, |
| 1281 | scrollTop = Chat.e.messagesWrapper.scrollTop; |
| 1282 | F.fetch("chat-poll",{ |
| 1283 | urlParams:{ |
| 1284 | before: Chat.mnMsg, |
| 1285 | n: n |
| 1286 | }, |
| @@ -1291,10 +1533,11 @@ | |
| 1291 | }, |
| 1292 | onload:function(x){ |
| 1293 | let gotMessages = x.msgs.length; |
| 1294 | newcontent(x,true); |
| 1295 | Chat._isBatchLoading = false; |
| 1296 | if(Chat._gotServerError){ |
| 1297 | Chat._gotServerError = false; |
| 1298 | return; |
| 1299 | } |
| 1300 | if(n<0/*we asked for all history*/ |
| @@ -1314,17 +1557,17 @@ | |
| 1314 | } |
| 1315 | if(gotMessages > 0){ |
| 1316 | F.toast.message("Loaded "+gotMessages+" older messages."); |
| 1317 | /* Return scroll position to where it was when the history load |
| 1318 | was requested, per user request */ |
| 1319 | Chat.e.messagesWrapper.scrollTo( |
| 1320 | 0, Chat.e.messagesWrapper.scrollHeight - scrollHt + scrollTop |
| 1321 | ); |
| 1322 | } |
| 1323 | }, |
| 1324 | aftersend:function(){ |
| 1325 | Chat.e.messagesWrapper.classList.remove('loading'); |
| 1326 | Chat.ajaxEnd(); |
| 1327 | } |
| 1328 | }); |
| 1329 | }; |
| 1330 | const wrapper = D.div(); /* browsers don't all properly handle >1 child in a fieldset */; |
| @@ -1333,19 +1576,19 @@ | |
| 1333 | D.append(wrapper, btn); |
| 1334 | btn.addEventListener('click',()=>loadOldMessages(Chat.loadMessageCount)); |
| 1335 | btn = D.button("All previous messages"); |
| 1336 | D.append(wrapper, btn); |
| 1337 | btn.addEventListener('click',()=>loadOldMessages(-1)); |
| 1338 | D.append(Chat.e.messagesWrapper, toolbar); |
| 1339 | toolbar.disabled = true /*will be enabled when msg load finishes */; |
| 1340 | })()/*end history loading widget setup*/; |
| 1341 | |
| 1342 | const afterFetch = function f(){ |
| 1343 | if(true===f.isFirstCall){ |
| 1344 | f.isFirstCall = false; |
| 1345 | Chat.ajaxEnd(); |
| 1346 | Chat.e.messagesWrapper.classList.remove('loading'); |
| 1347 | setTimeout(function(){ |
| 1348 | Chat.scrollMessagesTo(1); |
| 1349 | }, 250); |
| 1350 | } |
| 1351 | if(Chat._gotServerError && Chat.intervalTimer){ |
| @@ -1363,11 +1606,11 @@ | |
| 1363 | f.running = true; |
| 1364 | Chat._isBatchLoading = f.isFirstCall; |
| 1365 | if(true===f.isFirstCall){ |
| 1366 | f.isFirstCall = false; |
| 1367 | Chat.ajaxStart(); |
| 1368 | Chat.e.messagesWrapper.classList.add('loading'); |
| 1369 | } |
| 1370 | F.fetch("chat-poll",{ |
| 1371 | timeout: 420 * 1000/*FIXME: get the value from the server*/, |
| 1372 | urlParams:{ |
| 1373 | name: Chat.mxMsg |
| @@ -1384,11 +1627,14 @@ | |
| 1384 | resumed, and reportError() produces a loud error message. */ |
| 1385 | afterFetch(); |
| 1386 | }, |
| 1387 | onload:function(y){ |
| 1388 | newcontent(y); |
| 1389 | Chat._isBatchLoading = false; |
| 1390 | afterFetch(); |
| 1391 | } |
| 1392 | }); |
| 1393 | }; |
| 1394 | poll.isFirstCall = true; |
| @@ -1395,7 +1641,15 @@ | |
| 1395 | Chat._gotServerError = poll.running = false; |
| 1396 | if( window.fossil.config.chat.fromcli ){ |
| 1397 | Chat.chatOnlyMode(true); |
| 1398 | } |
| 1399 | Chat.intervalTimer = setInterval(poll, 1000); |
| 1400 | F.page.chat = Chat/* enables testing the APIs via the dev tools */; |
| 1401 | })(); |
| 1402 |
| --- src/chat.js | |
| +++ src/chat.js | |
| @@ -34,10 +34,30 @@ | |
| 34 | else if(r1.bottom<=r2.bottom && r1.bottom>=r2.top) return true; |
| 35 | return false; |
| 36 | }; |
| 37 | |
| 38 | const addAnchorTargetBlank = (e)=>D.attr(e, 'target','_blank'); |
| 39 | |
| 40 | /** |
| 41 | Returns an almost-ISO8601 form of Date object d. |
| 42 | */ |
| 43 | const iso8601ish = function(d){ |
| 44 | return d.toISOString() |
| 45 | .replace('T',' ').replace(/\.\d+/,'') |
| 46 | .replace('Z', ' zulu'); |
| 47 | }; |
| 48 | /** Returns the local time string of Date object d, defaulting |
| 49 | to the current time. */ |
| 50 | const localTimeString = function ff(d){ |
| 51 | d || (d = new Date()); |
| 52 | return [ |
| 53 | d.getFullYear(),'-',pad2(d.getMonth()+1/*sigh*/), |
| 54 | '-',pad2(d.getDate()), |
| 55 | ' ',pad2(d.getHours()),':',pad2(d.getMinutes()), |
| 56 | ':',pad2(d.getSeconds()) |
| 57 | ].join(''); |
| 58 | }; |
| 59 | |
| 60 | (function(){ |
| 61 | let dbg = document.querySelector('#debugMsg'); |
| 62 | if(dbg){ |
| 63 | /* This can inadvertently influence our flexbox layouts, so move |
| @@ -74,11 +94,11 @@ | |
| 94 | ht = wh - extra; |
| 95 | } |
| 96 | f.contentArea.style.height = |
| 97 | f.contentArea.style.maxHeight = [ |
| 98 | "calc(", (ht>=100 ? ht : 100), "px", |
| 99 | " - 0.75em"/*fudge value*/,")" |
| 100 | /* ^^^^ hypothetically not needed, but both Chrome/FF on |
| 101 | Linux will force scrollbars on the body if this value is |
| 102 | too small (<0.75em in my tests). */ |
| 103 | ].join(''); |
| 104 | if(false){ |
| @@ -105,21 +125,24 @@ | |
| 125 | pageTitle: E1('head title'), |
| 126 | loadOlderToolbar: undefined /* the load-posts toolbar (dynamically created) */, |
| 127 | inputWrapper: E1("#chat-input-area"), |
| 128 | inputLine: E1('#chat-input-line'), |
| 129 | fileSelectWrapper: E1('#chat-input-file-area'), |
| 130 | viewMessages: E1('#chat-messages-wrapper'), |
| 131 | btnSubmit: E1('#chat-message-submit'), |
| 132 | inputSingle: E1('#chat-input-single'), |
| 133 | inputMulti: E1('#chat-input-multi'), |
| 134 | inputCurrent: undefined/*one of inputSingle or inputMulti*/, |
| 135 | inputFile: E1('#chat-input-file'), |
| 136 | contentDiv: E1('div.content'), |
| 137 | viewConfig: E1('#chat-config'), |
| 138 | viewPreview: E1('#chat-preview'), |
| 139 | previewContent: E1('#chat-preview-content'), |
| 140 | btnPreview: E1('#chat-preview-button'), |
| 141 | views: document.querySelectorAll('.chat-view'), |
| 142 | activeUserListWrapper: E1('#chat-user-list-wrapper'), |
| 143 | activeUserList: E1('#chat-user-list') |
| 144 | }, |
| 145 | me: F.user.name, |
| 146 | mxMsg: F.config.chat.initSize ? -F.config.chat.initSize : -50, |
| 147 | mnMsg: undefined/*lowest message ID we've seen so far (for history loading)*/, |
| 148 | pageIsActive: 'visible'===document.visibilityState, |
| @@ -127,10 +150,23 @@ | |
| 150 | notificationBubbleColor: 'white', |
| 151 | totalMessageCount: 0, // total # of inbound messages |
| 152 | //! Number of messages to load for the history buttons |
| 153 | loadMessageCount: Math.abs(F.config.chat.initSize || 20), |
| 154 | ajaxInflight: 0, |
| 155 | usersLastSeen:{ |
| 156 | /* Map of user names to their most recent message time |
| 157 | (JS Date object). Only messages received by the chat client |
| 158 | are considered. */ |
| 159 | /* Reminder: to convert a Julian time J to JS: |
| 160 | new Date((J - 2440587.5) * 86400000) */ |
| 161 | }, |
| 162 | filterState:{ |
| 163 | activeUser: undefined, |
| 164 | match: function(uname){ |
| 165 | return this.activeUser===uname || !this.activeUser; |
| 166 | } |
| 167 | }, |
| 168 | /** Gets (no args) or sets (1 arg) the current input text field value, |
| 169 | taking into account single- vs multi-line input. The getter returns |
| 170 | a string and the setter returns this object. */ |
| 171 | inputValue: function(){ |
| 172 | const e = this.inputElement(); |
| @@ -157,19 +193,20 @@ | |
| 193 | this.e.inputLine.classList.remove('single-line'); |
| 194 | }else{ |
| 195 | this.e.inputCurrent = this.e.inputSingle; |
| 196 | this.e.inputLine.classList.add('single-line'); |
| 197 | } |
| 198 | const m = this.e.viewMessages, |
| 199 | sTop = m.scrollTop, |
| 200 | mh1 = m.clientHeight; |
| 201 | D.addClass(old, 'hidden'); |
| 202 | D.removeClass(this.e.inputCurrent, 'hidden'); |
| 203 | const mh2 = m.clientHeight; |
| 204 | m.scrollTo(0, sTop + (mh1-mh2)); |
| 205 | this.e.inputCurrent.value = old.value; |
| 206 | old.value = ''; |
| 207 | this.animate(this.e.inputCurrent, "anim-flip-v"); |
| 208 | return this; |
| 209 | }, |
| 210 | /** |
| 211 | If passed true or no arguments, switches to multi-line mode |
| 212 | if currently in single-line mode. If passed false, switches |
| @@ -241,12 +278,15 @@ | |
| 278 | /* Injects DOM element e as a new row in the chat, at the oldest |
| 279 | end of the list if atEnd is truthy, else at the newest end of |
| 280 | the list. */ |
| 281 | injectMessageElem: function f(e, atEnd){ |
| 282 | const mip = atEnd ? this.e.loadOlderToolbar : this.e.messageInjectPoint, |
| 283 | holder = this.e.viewMessages, |
| 284 | prevMessage = this.e.newestMessage; |
| 285 | if(!this.filterState.match(e.dataset.xfrom)){ |
| 286 | e.classList.add('hidden'); |
| 287 | } |
| 288 | if(atEnd){ |
| 289 | const fe = mip.nextElementSibling; |
| 290 | if(fe) mip.parentNode.insertBefore(e, fe); |
| 291 | else D.append(mip.parentNode, e); |
| 292 | }else{ |
| @@ -337,22 +377,22 @@ | |
| 377 | <0 = top of the message list, >0 = bottom of the message list, |
| 378 | 0 == the newest message (normally the same position as >1). |
| 379 | */ |
| 380 | scrollMessagesTo: function(where){ |
| 381 | if(where<0){ |
| 382 | Chat.e.viewMessages.scrollTop = 0; |
| 383 | }else if(where>0){ |
| 384 | Chat.e.viewMessages.scrollTop = Chat.e.viewMessages.scrollHeight; |
| 385 | }else if(Chat.e.newestMessage){ |
| 386 | Chat.e.newestMessage.scrollIntoView(false); |
| 387 | } |
| 388 | }, |
| 389 | toggleChatOnlyMode: function(){ |
| 390 | return this.chatOnlyMode(!this.isChatOnlyMode()); |
| 391 | }, |
| 392 | messageIsInView: function(e){ |
| 393 | return e ? overlapsElemView(e, this.e.viewMessages) : false; |
| 394 | }, |
| 395 | settings:{ |
| 396 | get: (k,dflt)=>F.storage.get(k,dflt), |
| 397 | getBool: (k,dflt)=>F.storage.getBool(k,dflt), |
| 398 | set: (k,v)=>F.storage.set(k,v), |
| @@ -366,11 +406,13 @@ | |
| 406 | defaults:{ |
| 407 | "images-inline": !!F.config.chat.imagesInline, |
| 408 | "edit-multiline": false, |
| 409 | "monospace-messages": false, |
| 410 | "chat-only-mode": false, |
| 411 | "audible-alert": true, |
| 412 | "active-user-list": false, |
| 413 | "active-user-list-timestamps": false |
| 414 | } |
| 415 | }, |
| 416 | /** Plays a new-message notification sound IF the audible-alert |
| 417 | setting is true, else this is a no-op. Returns this. |
| 418 | */ |
| @@ -395,12 +437,117 @@ | |
| 437 | setNewMessageSound: function f(uri){ |
| 438 | delete this.playNewMessageSound.audio; |
| 439 | this.playNewMessageSound.uri = uri; |
| 440 | this.settings.set('audible-alert', uri); |
| 441 | return this; |
| 442 | }, |
| 443 | /** |
| 444 | Expects e to be one of the elements in this.e.views. |
| 445 | The 'hidden' class is removed from e and added to |
| 446 | all other elements in that list. Returns e. |
| 447 | */ |
| 448 | setCurrentView: function(e){ |
| 449 | if(e===this.e.currentView){ |
| 450 | return e; |
| 451 | } |
| 452 | this.e.views.forEach(function(E){ |
| 453 | if(e!==E) D.addClass(E,'hidden'); |
| 454 | }); |
| 455 | this.e.currentView = D.removeClass(e,'hidden'); |
| 456 | this.animate(this.e.currentView, 'anim-fade-in-fast'); |
| 457 | return this.e.currentView; |
| 458 | }, |
| 459 | /** |
| 460 | Updates the "active user list" view if we are not currently |
| 461 | batch-loading messages and if the active user list UI element |
| 462 | is active. |
| 463 | */ |
| 464 | updateActiveUserList: function callee(){ |
| 465 | if(this._isBatchLoading |
| 466 | || this.e.activeUserListWrapper.classList.contains('hidden')){ |
| 467 | return this; |
| 468 | }else if(!callee.sortUsersSeen){ |
| 469 | /** Array.sort() callback. Expects an array of user names and |
| 470 | sorts them in last-received message order (newest first). */ |
| 471 | const self = this; |
| 472 | callee.sortUsersSeen = function(l,r){ |
| 473 | l = self.usersLastSeen[l]; |
| 474 | r = self.usersLastSeen[r]; |
| 475 | if(l && r) return r - l; |
| 476 | else if(l) return -1; |
| 477 | else if(r) return 1; |
| 478 | else return 0; |
| 479 | }; |
| 480 | callee.addUserElem = function(u){ |
| 481 | const uSpan = D.addClass(D.span(), 'chat-user'); |
| 482 | const uDate = self.usersLastSeen[u]; |
| 483 | if(self.filterState.activeUser===u){ |
| 484 | uSpan.classList.add('selected'); |
| 485 | } |
| 486 | uSpan.dataset.uname = u; |
| 487 | D.append(uSpan, u, "\n", |
| 488 | D.append( |
| 489 | D.addClass(D.span(),'timestamp'), |
| 490 | localTimeString(uDate)//.substr(5/*chop off year*/) |
| 491 | )); |
| 492 | if(uDate.$uColor){ |
| 493 | uSpan.style.backgroundColor = uDate.$uColor; |
| 494 | } |
| 495 | D.append(self.e.activeUserList, uSpan); |
| 496 | }; |
| 497 | } |
| 498 | //D.clearElement(this.e.activeUserList); |
| 499 | D.remove(this.e.activeUserList.querySelectorAll('.chat-user')); |
| 500 | Object.keys(this.usersLastSeen).sort( |
| 501 | callee.sortUsersSeen |
| 502 | ).forEach(callee.addUserElem); |
| 503 | return this; |
| 504 | }, |
| 505 | /** |
| 506 | Applies user name filter to all current messages, or clears |
| 507 | the filter if uname is falsy. |
| 508 | */ |
| 509 | setUserFilter: function(uname){ |
| 510 | this.filterState.activeUser = uname; |
| 511 | const mw = this.e.viewMessages.querySelectorAll('.message-widget'); |
| 512 | const self = this; |
| 513 | let eLast; |
| 514 | if(!uname){ |
| 515 | D.removeClass(Chat.e.viewMessages.querySelectorAll('.message-widget.hidden'), |
| 516 | 'hidden'); |
| 517 | }else{ |
| 518 | mw.forEach(function(w){ |
| 519 | if(self.filterState.match(w.dataset.xfrom)){ |
| 520 | w.classList.remove('hidden'); |
| 521 | eLast = w; |
| 522 | }else{ |
| 523 | w.classList.add('hidden'); |
| 524 | } |
| 525 | }); |
| 526 | } |
| 527 | if(eLast) eLast.scrollIntoView(false); |
| 528 | else this.scrollMessagesTo(1); |
| 529 | cs.e.activeUserList.querySelectorAll('.chat-user').forEach(function(e){ |
| 530 | e.classList[uname===e.dataset.uname ? 'add' : 'remove']('selected'); |
| 531 | }); |
| 532 | return this; |
| 533 | }, |
| 534 | |
| 535 | /** |
| 536 | If animations are enabled, passes its arguments |
| 537 | to D.addClassBriefly(), else this is a no-op. |
| 538 | If cb is a function, it is called after the |
| 539 | CSS class is removed. Returns this object; |
| 540 | */ |
| 541 | animate: function f(e,a,cb){ |
| 542 | if(!f.$disabled){ |
| 543 | D.addClassBriefly(e, a, 0, cb); |
| 544 | } |
| 545 | return this; |
| 546 | } |
| 547 | }; |
| 548 | cs.animate.$disabled = true; |
| 549 | F.fetch.beforesend = ()=>cs.ajaxStart(); |
| 550 | F.fetch.aftersend = ()=>cs.ajaxEnd(); |
| 551 | cs.e.inputCurrent = cs.e.inputSingle; |
| 552 | /* Install default settings... */ |
| 553 | Object.keys(cs.settings.defaults).forEach(function(k){ |
| @@ -415,10 +562,16 @@ | |
| 562 | tall vs wide. Can be toggled via settings popup. */ |
| 563 | document.body.classList.add('my-messages-right'); |
| 564 | } |
| 565 | if(cs.settings.getBool('monospace-messages',false)){ |
| 566 | document.body.classList.add('monospace-messages'); |
| 567 | } |
| 568 | if(cs.settings.getBool('active-user-list',false)){ |
| 569 | cs.e.activeUserListWrapper.classList.remove('hidden'); |
| 570 | } |
| 571 | if(cs.settings.getBool('active-user-list-timestamps',false)){ |
| 572 | cs.e.activeUserList.classList.add('timestamps'); |
| 573 | } |
| 574 | cs.inputMultilineMode(cs.settings.getBool('edit-multiline',false)); |
| 575 | cs.chatOnlyMode(cs.settings.getBool('chat-only-mode')); |
| 576 | cs.pageTitleOrig = cs.e.pageTitle.innerText; |
| 577 | const qs = (e)=>document.querySelector(e); |
| @@ -611,10 +764,37 @@ | |
| 764 | cs.pageIsActive = ('visible' === document.visibilityState); |
| 765 | if(cs.pageIsActive){ |
| 766 | cs.e.pageTitle.innerText = cs.pageTitleOrig; |
| 767 | } |
| 768 | }, true); |
| 769 | cs.setCurrentView(cs.e.viewMessages); |
| 770 | |
| 771 | cs.e.activeUserList.addEventListener('click', function f(ev){ |
| 772 | /* Filter messages on a user clicked in activeUserList */ |
| 773 | ev.stopPropagation(); |
| 774 | ev.preventDefault(); |
| 775 | let eUser = ev.target; |
| 776 | while(eUser!==this && !eUser.classList.contains('chat-user')){ |
| 777 | eUser = eUser.parentNode; |
| 778 | } |
| 779 | if(eUser==this || !eUser) return false; |
| 780 | const uname = eUser.dataset.uname; |
| 781 | let eLast; |
| 782 | cs.setCurrentView(cs.e.viewMessages); |
| 783 | if(eUser.classList.contains('selected')){ |
| 784 | /* If curently selected, toggle filter off */ |
| 785 | eUser.classList.remove('selected'); |
| 786 | cs.setUserFilter(false); |
| 787 | delete f.$eSelected; |
| 788 | }else{ |
| 789 | if(f.$eSelected) f.$eSelected.classList.remove('selected'); |
| 790 | f.$eSelected = eUser; |
| 791 | eUser.classList.add('selected'); |
| 792 | cs.setUserFilter(uname); |
| 793 | } |
| 794 | return false; |
| 795 | }, false); |
| 796 | return cs; |
| 797 | })()/*Chat initialization*/; |
| 798 | |
| 799 | /** |
| 800 | Custom widget type for rendering messages (one message per |
| @@ -658,21 +838,10 @@ | |
| 838 | d.getHours(),":", |
| 839 | (d.getMinutes()+100).toString().slice(1,3), |
| 840 | ' ', dowMap[d.getDay()] |
| 841 | ].join(''); |
| 842 | }; |
| 843 | cf.prototype = { |
| 844 | scrollIntoView: function(){ |
| 845 | this.e.content.scrollIntoView(); |
| 846 | }, |
| 847 | setMessage: function(m){ |
| @@ -758,14 +927,14 @@ | |
| 927 | eXFrom.addEventListener('click', ()=>this.e.tab.click(), false); |
| 928 | }*/ |
| 929 | return this; |
| 930 | }, |
| 931 | /* Event handler for clicking .message-user elements to show their |
| 932 | timestamps and a set of actions. */ |
| 933 | _handleLegendClicked: function f(ev){ |
| 934 | if(!f.popup){ |
| 935 | /* "Popup" widget */ |
| 936 | f.popup = { |
| 937 | e: D.addClass(D.div(), 'chat-message-popup'), |
| 938 | refresh:function(){ |
| 939 | const eMsg = this.$eMsg/*.message-widget element*/; |
| 940 | if(!eMsg) return; |
| @@ -830,18 +999,45 @@ | |
| 999 | y: 'a' |
| 1000 | }), "User's Timeline"), |
| 1001 | 'target', '_blank' |
| 1002 | ); |
| 1003 | D.append(toolbar2, timelineLink); |
| 1004 | if(Chat.filterState.activeUser && |
| 1005 | Chat.filterState.match(eMsg.dataset.xfrom)){ |
| 1006 | /* Add a button to clear user filter and jump to |
| 1007 | this message in its original context. */ |
| 1008 | D.append( |
| 1009 | this.e, |
| 1010 | D.append( |
| 1011 | D.addClass(D.div(), 'toolbar'), |
| 1012 | D.button( |
| 1013 | "Message in context", |
| 1014 | function(){ |
| 1015 | self.hide(); |
| 1016 | Chat.setUserFilter(false); |
| 1017 | eMsg.scrollIntoView(false); |
| 1018 | Chat.animate( |
| 1019 | eMsg.firstElementChild, 'anim-flip-h' |
| 1020 | //eMsg.firstElementChild, 'anim-flip-v' |
| 1021 | //eMsg.childNodes, 'anim-rotate-360' |
| 1022 | //eMsg.childNodes, 'anim-flip-v' |
| 1023 | //eMsg, 'anim-flip-v' |
| 1024 | ); |
| 1025 | }) |
| 1026 | ) |
| 1027 | ); |
| 1028 | }/*jump-to button*/ |
| 1029 | } |
| 1030 | const tab = eMsg.querySelector('.message-widget-tab'); |
| 1031 | D.append(tab, this.e); |
| 1032 | D.removeClass(this.e, 'hidden'); |
| 1033 | Chat.animate(this.e, 'anim-fade-in-fast'); |
| 1034 | }/*refresh()*/, |
| 1035 | hide: function(){ |
| 1036 | delete this.$eMsg; |
| 1037 | D.addClass(this.e, 'hidden'); |
| 1038 | D.clearElement(this.e); |
| 1039 | }, |
| 1040 | show: function(tgtMsg){ |
| 1041 | if(tgtMsg === this.$eMsg){ |
| 1042 | this.hide(); |
| 1043 | return; |
| @@ -961,11 +1157,11 @@ | |
| 1157 | */ |
| 1158 | Chat.submitMessage = function f(){ |
| 1159 | if(!f.spaces){ |
| 1160 | f.spaces = /\s+$/; |
| 1161 | } |
| 1162 | this.setCurrentView(this.e.viewMessages); |
| 1163 | const fd = new FormData(); |
| 1164 | var msg = this.inputValue().trim(); |
| 1165 | if(msg && (msg.indexOf('\n')>0 || f.spaces.test(msg))){ |
| 1166 | /* Cosmetic: trim whitespace from the ends of lines to try to |
| 1167 | keep copy/paste from terminals, especially wide ones, from |
| @@ -1021,42 +1217,102 @@ | |
| 1217 | e.preventDefault(); |
| 1218 | Chat.submitMessage(); |
| 1219 | return false; |
| 1220 | }); |
| 1221 | |
| 1222 | (function(){/*Set up #chat-settings-button */ |
| 1223 | const settingsButton = document.querySelector('#chat-settings-button'); |
| 1224 | const optionsMenu = E1('#chat-config-options'); |
| 1225 | const cbToggle = function(ev){ |
| 1226 | ev.preventDefault(); |
| 1227 | ev.stopPropagation(); |
| 1228 | Chat.setCurrentView(Chat.e.currentView===Chat.e.viewConfig |
| 1229 | ? Chat.e.viewMessages : Chat.e.viewConfig); |
| 1230 | return false; |
| 1231 | }; |
| 1232 | D.attr(settingsButton, 'role', 'button').addEventListener('click', cbToggle, false); |
| 1233 | Chat.e.viewConfig.querySelector('button').addEventListener('click', cbToggle, false); |
| 1234 | |
| 1235 | /** Internal acrobatics to allow certain settings toggles to access |
| 1236 | related toggles. */ |
| 1237 | const namedOptions = { |
| 1238 | activeUsers:{ |
| 1239 | label: "Show active users list", |
| 1240 | boolValue: ()=>!Chat.e.activeUserListWrapper.classList.contains('hidden'), |
| 1241 | persistentSetting: 'active-user-list', |
| 1242 | callback: function(){ |
| 1243 | D.toggleClass(Chat.e.activeUserListWrapper,'hidden'); |
| 1244 | D.removeClass(Chat.e.activeUserListWrapper, 'collapsed'); |
| 1245 | if(Chat.e.activeUserListWrapper.classList.contains('hidden')){ |
| 1246 | /* When hiding this element, undo all filtering */ |
| 1247 | Chat.setUserFilter(false); |
| 1248 | /*Ideally we'd scroll the final message into view |
| 1249 | now, but because viewMessages is currently hidden behind |
| 1250 | viewConfig, scrolling is a no-op. */ |
| 1251 | Chat.scrollMessagesTo(1); |
| 1252 | }else{ |
| 1253 | Chat.updateActiveUserList(); |
| 1254 | Chat.animate(Chat.e.activeUserListWrapper, 'anim-flip-v'); |
| 1255 | } |
| 1256 | } |
| 1257 | } |
| 1258 | }; |
| 1259 | if(1){ |
| 1260 | /* Per user request, toggle the list of users on and off if the |
| 1261 | legend element is tapped. */ |
| 1262 | const optAu = namedOptions.activeUsers; |
| 1263 | optAu.theLegend = Chat.e.activeUserListWrapper.firstElementChild/*LEGEND*/; |
| 1264 | optAu.theList = optAu.theLegend.nextElementSibling/*user list container*/; |
| 1265 | optAu.theLegend.addEventListener('click',function(){ |
| 1266 | D.toggleClass(Chat.e.activeUserListWrapper, 'collapsed'); |
| 1267 | if(!Chat.e.activeUserListWrapper.classList.contains('collapsed')){ |
| 1268 | Chat.animate(optAu.theList,'anim-flip-v'); |
| 1269 | } |
| 1270 | }, false); |
| 1271 | }/*namedOptions.activeUsers additional setup*/ |
| 1272 | /* Settings menu entries... Remember that they will be rendered in |
| 1273 | reverse order and the most frequently-needed ones "should" |
| 1274 | (arguably) be closer to the start of this list so that they |
| 1275 | will be rendered within easier reach of the settings button. */ |
| 1276 | const settingsOps = [{ |
| 1277 | label: "Multi-line input", |
| 1278 | boolValue: ()=>Chat.inputElement()===Chat.e.inputMulti, |
| 1279 | persistentSetting: 'edit-multiline', |
| 1280 | callback: function(){ |
| 1281 | Chat.inputToggleSingleMulti(); |
| 1282 | } |
| 1283 | },{ |
| 1284 | label: "Left-align my posts", |
| 1285 | boolValue: ()=>!document.body.classList.contains('my-messages-right'), |
| 1286 | callback: function f(){ |
| 1287 | document.body.classList.toggle('my-messages-right'); |
| 1288 | } |
| 1289 | },{ |
| 1290 | label: "Show images inline", |
| 1291 | boolValue: ()=>Chat.settings.getBool('images-inline'), |
| 1292 | callback: function(){ |
| 1293 | const v = Chat.settings.toggle('images-inline'); |
| 1294 | F.toast.message("Image mode set to "+(v ? "inline" : "hyperlink")+"."); |
| 1295 | } |
| 1296 | },{ |
| 1297 | label: "Timestamps in active users list", |
| 1298 | boolValue: ()=>Chat.e.activeUserList.classList.contains('timestamps'), |
| 1299 | persistentSetting: 'active-user-list-timestamps', |
| 1300 | callback: function(){ |
| 1301 | D.toggleClass(Chat.e.activeUserList,'timestamps'); |
| 1302 | /* If the timestamp option is activated but |
| 1303 | namedOptions.activeUsers is not currently checked then |
| 1304 | toggle that option on as well. */ |
| 1305 | if(Chat.e.activeUserList.classList.contains('timestamps') |
| 1306 | && !namedOptions.activeUsers.boolValue()){ |
| 1307 | namedOptions.activeUsers.checkbox.checked = true; |
| 1308 | namedOptions.activeUsers.callback(); |
| 1309 | Chat.settings.set(namedOptions.activeUsers.persistentSetting, true); |
| 1310 | } |
| 1311 | } |
| 1312 | }, |
| 1313 | namedOptions.activeUsers,{ |
| 1314 | label: "Monospace message font", |
| 1315 | boolValue: ()=>document.body.classList.contains('monospace-messages'), |
| 1316 | persistentSetting: 'monospace-messages', |
| 1317 | callback: function(){ |
| 1318 | document.body.classList.toggle('monospace-messages'); |
| @@ -1066,57 +1322,44 @@ | |
| 1322 | boolValue: ()=>Chat.isChatOnlyMode(), |
| 1323 | persistentSetting: 'chat-only-mode', |
| 1324 | callback: function(){ |
| 1325 | Chat.toggleChatOnlyMode(); |
| 1326 | } |
| 1327 | }]; |
| 1328 | |
| 1329 | /** Set up selection list of notification sounds. */ |
| 1330 | if(1){ |
| 1331 | const selectSound = D.select(); |
| 1332 | D.option(selectSound, "", "(no audio)"); |
| 1333 | const firstSoundIndex = selectSound.options.length; |
| 1334 | F.config.chat.alerts.forEach((a)=>D.option(selectSound, a)); |
| 1335 | if(true===Chat.settings.getBool('audible-alert')){ |
| 1336 | /* This setting used to be a plain bool. If we encounter |
| 1337 | such a setting, take the first sound in the list. */ |
| 1338 | selectSound.selectedIndex = firstSoundIndex; |
| 1339 | }else{ |
| 1340 | selectSound.value = Chat.settings.get('audible-alert',''); |
| 1341 | if(selectSound.selectedIndex<0){ |
| 1342 | /* Missing file - removed after this setting was |
| 1343 | applied. Fall back to the first sound in the list. */ |
| 1344 | selectSound.selectedIndex = firstSoundIndex; |
| 1345 | } |
| 1346 | } |
| 1347 | Chat.setNewMessageSound(selectSound.value); |
| 1348 | settingsOps.push({ |
| 1349 | label: "Audio alert", |
| 1350 | select: selectSound, |
| 1351 | callback: function(ev){ |
| 1352 | const v = ev.target.value; |
| 1353 | Chat.setNewMessageSound(v); |
| 1354 | F.toast.message("Audio notifications "+(v ? "enabled" : "disabled")+"."); |
| 1355 | if(v) setTimeout(()=>Chat.playNewMessageSound(), 0); |
| 1356 | } |
| 1357 | }); |
| 1358 | }/*audio notification config*/ |
| 1359 | /** |
| 1360 | Build UI for config options... |
| 1361 | */ |
| 1362 | settingsOps.forEach(function f(op){ |
| 1363 | const line = D.addClass(D.div(), 'menu-entry'); |
| 1364 | const btn = D.append( |
| 1365 | D.addClass(D.label(), 'cbutton'/*bootstrap skin hijacks 'button'*/), |
| @@ -1125,62 +1368,52 @@ | |
| 1368 | op.callback(ev); |
| 1369 | if(op.persistentSetting){ |
| 1370 | Chat.settings.set(op.persistentSetting, op.boolValue()); |
| 1371 | } |
| 1372 | }; |
| 1373 | if(op.hasOwnProperty('select')){ |
| 1374 | D.append(line, btn, op.select); |
| 1375 | op.select.addEventListener('change', callback, false); |
| 1376 | }else if(op.hasOwnProperty('boolValue')){ |
| 1377 | if(undefined === f.$id) f.$id = 0; |
| 1378 | ++f.$id; |
| 1379 | const check = op.checkbox |
| 1380 | = D.attr(D.checkbox(1, op.boolValue()), |
| 1381 | 'aria-label', op.label); |
| 1382 | const id = 'cfgopt'+f.$id; |
| 1383 | if(op.boolValue()) check.checked = true; |
| 1384 | D.attr(check, 'id', id); |
| 1385 | D.attr(btn, 'for', id); |
| 1386 | D.append(line, check); |
| 1387 | check.addEventListener('change', callback); |
| 1388 | D.append(line, btn); |
| 1389 | }else{ |
| 1390 | line.addEventListener('click', callback); |
| 1391 | D.append(line, btn); |
| 1392 | } |
| 1393 | D.append(optionsMenu, line); |
| 1394 | }); |
| 1395 | if(0 && settingsOps.selectSound){ |
| 1396 | D.append(optionsMenu, settingsOps.selectSound); |
| 1397 | } |
| 1398 | //settingsButton.click()/*for for development*/; |
| 1399 | })()/*#chat-settings-button setup*/; |
| 1400 | |
| 1401 | (function(){/*set up message preview*/ |
| 1402 | const btnPreview = Chat.e.btnPreview; |
| 1403 | Chat.setPreviewText = function(t){ |
| 1404 | this.setCurrentView(this.e.viewPreview); |
| 1405 | this.e.previewContent.innerHTML = t; |
| 1406 | this.e.viewPreview.querySelectorAll('a').forEach(addAnchorTargetBlank); |
| 1407 | this.e.inputCurrent.focus(); |
| 1408 | }; |
| 1409 | Chat.e.viewPreview.querySelector('#chat-preview-close'). |
| 1410 | addEventListener('click', ()=>Chat.setCurrentView(Chat.e.viewMessages), false); |
| 1411 | let previewPending = false; |
| 1412 | const elemsToEnable = [ |
| 1413 | btnPreview, Chat.e.btnSubmit, |
| 1414 | Chat.e.inputSingle, Chat.e.inputMulti]; |
| 1415 | const submit = function(ev){ |
| 1416 | ev.preventDefault(); |
| 1417 | ev.stopPropagation(); |
| 1418 | if(previewPending) return false; |
| 1419 | const txt = Chat.e.inputCurrent.value; |
| @@ -1209,12 +1442,12 @@ | |
| 1442 | previewPending = true; |
| 1443 | Chat.setPreviewText("Loading preview..."); |
| 1444 | }, |
| 1445 | aftersend:function(){ |
| 1446 | previewPending = false; |
| 1447 | Chat.ajaxEnd(); |
| 1448 | D.enable(elemsToEnable); |
| 1449 | } |
| 1450 | }); |
| 1451 | return false; |
| 1452 | }; |
| 1453 | btnPreview.addEventListener('click', submit, false); |
| @@ -1231,10 +1464,18 @@ | |
| 1464 | should only be true when loading older messages. */ |
| 1465 | f.processPost = function(m,atEnd){ |
| 1466 | ++Chat.totalMessageCount; |
| 1467 | if( m.msgid>Chat.mxMsg ) Chat.mxMsg = m.msgid; |
| 1468 | if( !Chat.mnMsg || m.msgid<Chat.mnMsg) Chat.mnMsg = m.msgid; |
| 1469 | if(m.xfrom && m.mtime){ |
| 1470 | const d = new Date(m.mtime); |
| 1471 | const uls = Chat.usersLastSeen[m.xfrom]; |
| 1472 | if(!uls || uls<d){ |
| 1473 | d.$uColor = m.uclr; |
| 1474 | Chat.usersLastSeen[m.xfrom] = d; |
| 1475 | } |
| 1476 | } |
| 1477 | if( m.mdel ){ |
| 1478 | /* A record deletion notice. */ |
| 1479 | Chat.deleteMessageElem(m.mdel); |
| 1480 | return; |
| 1481 | } |
| @@ -1247,10 +1488,11 @@ | |
| 1488 | Chat._gotServerError = m; |
| 1489 | } |
| 1490 | }/*processPost()*/; |
| 1491 | }/*end static init*/ |
| 1492 | jx.msgs.forEach((m)=>f.processPost(m,atEnd)); |
| 1493 | Chat.updateActiveUserList(); |
| 1494 | if('visible'===document.visibilityState){ |
| 1495 | if(Chat.changesSincePageHidden){ |
| 1496 | Chat.changesSincePageHidden = 0; |
| 1497 | Chat.e.pageTitle.innerText = Chat.pageTitleOrig; |
| 1498 | } |
| @@ -1273,14 +1515,14 @@ | |
| 1515 | D.fieldset(loadLegend), "id", "load-msg-toolbar" |
| 1516 | ); |
| 1517 | Chat.disableDuringAjax.push(toolbar); |
| 1518 | /* Loads the next n oldest messages, or all previous history if n is negative. */ |
| 1519 | const loadOldMessages = function(n){ |
| 1520 | Chat.e.viewMessages.classList.add('loading'); |
| 1521 | Chat._isBatchLoading = true; |
| 1522 | const scrollHt = Chat.e.viewMessages.scrollHeight, |
| 1523 | scrollTop = Chat.e.viewMessages.scrollTop; |
| 1524 | F.fetch("chat-poll",{ |
| 1525 | urlParams:{ |
| 1526 | before: Chat.mnMsg, |
| 1527 | n: n |
| 1528 | }, |
| @@ -1291,10 +1533,11 @@ | |
| 1533 | }, |
| 1534 | onload:function(x){ |
| 1535 | let gotMessages = x.msgs.length; |
| 1536 | newcontent(x,true); |
| 1537 | Chat._isBatchLoading = false; |
| 1538 | Chat.updateActiveUserList(); |
| 1539 | if(Chat._gotServerError){ |
| 1540 | Chat._gotServerError = false; |
| 1541 | return; |
| 1542 | } |
| 1543 | if(n<0/*we asked for all history*/ |
| @@ -1314,17 +1557,17 @@ | |
| 1557 | } |
| 1558 | if(gotMessages > 0){ |
| 1559 | F.toast.message("Loaded "+gotMessages+" older messages."); |
| 1560 | /* Return scroll position to where it was when the history load |
| 1561 | was requested, per user request */ |
| 1562 | Chat.e.viewMessages.scrollTo( |
| 1563 | 0, Chat.e.viewMessages.scrollHeight - scrollHt + scrollTop |
| 1564 | ); |
| 1565 | } |
| 1566 | }, |
| 1567 | aftersend:function(){ |
| 1568 | Chat.e.viewMessages.classList.remove('loading'); |
| 1569 | Chat.ajaxEnd(); |
| 1570 | } |
| 1571 | }); |
| 1572 | }; |
| 1573 | const wrapper = D.div(); /* browsers don't all properly handle >1 child in a fieldset */; |
| @@ -1333,19 +1576,19 @@ | |
| 1576 | D.append(wrapper, btn); |
| 1577 | btn.addEventListener('click',()=>loadOldMessages(Chat.loadMessageCount)); |
| 1578 | btn = D.button("All previous messages"); |
| 1579 | D.append(wrapper, btn); |
| 1580 | btn.addEventListener('click',()=>loadOldMessages(-1)); |
| 1581 | D.append(Chat.e.viewMessages, toolbar); |
| 1582 | toolbar.disabled = true /*will be enabled when msg load finishes */; |
| 1583 | })()/*end history loading widget setup*/; |
| 1584 | |
| 1585 | const afterFetch = function f(){ |
| 1586 | if(true===f.isFirstCall){ |
| 1587 | f.isFirstCall = false; |
| 1588 | Chat.ajaxEnd(); |
| 1589 | Chat.e.viewMessages.classList.remove('loading'); |
| 1590 | setTimeout(function(){ |
| 1591 | Chat.scrollMessagesTo(1); |
| 1592 | }, 250); |
| 1593 | } |
| 1594 | if(Chat._gotServerError && Chat.intervalTimer){ |
| @@ -1363,11 +1606,11 @@ | |
| 1606 | f.running = true; |
| 1607 | Chat._isBatchLoading = f.isFirstCall; |
| 1608 | if(true===f.isFirstCall){ |
| 1609 | f.isFirstCall = false; |
| 1610 | Chat.ajaxStart(); |
| 1611 | Chat.e.viewMessages.classList.add('loading'); |
| 1612 | } |
| 1613 | F.fetch("chat-poll",{ |
| 1614 | timeout: 420 * 1000/*FIXME: get the value from the server*/, |
| 1615 | urlParams:{ |
| 1616 | name: Chat.mxMsg |
| @@ -1384,11 +1627,14 @@ | |
| 1627 | resumed, and reportError() produces a loud error message. */ |
| 1628 | afterFetch(); |
| 1629 | }, |
| 1630 | onload:function(y){ |
| 1631 | newcontent(y); |
| 1632 | if(Chat._isBatchLoading){ |
| 1633 | Chat._isBatchLoading = false; |
| 1634 | Chat.updateActiveUserList(); |
| 1635 | } |
| 1636 | afterFetch(); |
| 1637 | } |
| 1638 | }); |
| 1639 | }; |
| 1640 | poll.isFirstCall = true; |
| @@ -1395,7 +1641,15 @@ | |
| 1641 | Chat._gotServerError = poll.running = false; |
| 1642 | if( window.fossil.config.chat.fromcli ){ |
| 1643 | Chat.chatOnlyMode(true); |
| 1644 | } |
| 1645 | Chat.intervalTimer = setInterval(poll, 1000); |
| 1646 | if(0){ |
| 1647 | const flip = (ev)=>Chat.animate(ev.target,'anim-flip-h'); |
| 1648 | document.querySelectorAll('#chat-edit-buttons button').forEach(function(e){ |
| 1649 | e.addEventListener('click',flip, false); |
| 1650 | }); |
| 1651 | } |
| 1652 | setTimeout( ()=>Chat.inputFocus(), 0 ); |
| 1653 | Chat.animate.$disabled = false; |
| 1654 | F.page.chat = Chat/* enables testing the APIs via the dev tools */; |
| 1655 | })(); |
| 1656 |
+4
| --- src/clone.c | ||
| +++ src/clone.c | ||
| @@ -265,10 +265,14 @@ | ||
| 265 | 265 | fossil_fatal("server returned an error - clone aborted"); |
| 266 | 266 | } |
| 267 | 267 | db_open_repository(zRepo); |
| 268 | 268 | } |
| 269 | 269 | db_begin_transaction(); |
| 270 | + if( db_exists("SELECT 1 FROM delta WHERE srcId IN phantom") ){ | |
| 271 | + fossil_fatal("there are unresolved deltas -" | |
| 272 | + " the clone is probably incomplete and unusable."); | |
| 273 | + } | |
| 270 | 274 | fossil_print("Rebuilding repository meta-data...\n"); |
| 271 | 275 | rebuild_db(0, 1, 0); |
| 272 | 276 | if( !noCompress ){ |
| 273 | 277 | fossil_print("Extra delta compression... "); fflush(stdout); |
| 274 | 278 | extra_deltification(); |
| 275 | 279 |
| --- src/clone.c | |
| +++ src/clone.c | |
| @@ -265,10 +265,14 @@ | |
| 265 | fossil_fatal("server returned an error - clone aborted"); |
| 266 | } |
| 267 | db_open_repository(zRepo); |
| 268 | } |
| 269 | db_begin_transaction(); |
| 270 | fossil_print("Rebuilding repository meta-data...\n"); |
| 271 | rebuild_db(0, 1, 0); |
| 272 | if( !noCompress ){ |
| 273 | fossil_print("Extra delta compression... "); fflush(stdout); |
| 274 | extra_deltification(); |
| 275 |
| --- src/clone.c | |
| +++ src/clone.c | |
| @@ -265,10 +265,14 @@ | |
| 265 | fossil_fatal("server returned an error - clone aborted"); |
| 266 | } |
| 267 | db_open_repository(zRepo); |
| 268 | } |
| 269 | db_begin_transaction(); |
| 270 | if( db_exists("SELECT 1 FROM delta WHERE srcId IN phantom") ){ |
| 271 | fossil_fatal("there are unresolved deltas -" |
| 272 | " the clone is probably incomplete and unusable."); |
| 273 | } |
| 274 | fossil_print("Rebuilding repository meta-data...\n"); |
| 275 | rebuild_db(0, 1, 0); |
| 276 | if( !noCompress ){ |
| 277 | fossil_print("Extra delta compression... "); fflush(stdout); |
| 278 | extra_deltification(); |
| 279 |
+17
-8
| --- src/default.css | ||
| +++ src/default.css | ||
| @@ -602,11 +602,16 @@ | ||
| 602 | 602 | tr.diffskip > td.chunkctrl { |
| 603 | 603 | text-align: left; |
| 604 | 604 | font-family: monospace; |
| 605 | 605 | } |
| 606 | 606 | tr.diffskip > td.chunkctrl > div { |
| 607 | - /* Exists solely for layout purposes. */ | |
| 607 | + display: flex; | |
| 608 | + align-items: center; | |
| 609 | +} | |
| 610 | +tr.diffskip > td.chunkctrl > div > span.error { | |
| 611 | + padding: 0.25em 0.5em; | |
| 612 | + border-radius: 0.5em; | |
| 608 | 613 | } |
| 609 | 614 | tr.diffskip > td.chunkctrl .jcbutton |
| 610 | 615 | /* class name .button breaks w/ some skins! */ { |
| 611 | 616 | min-width: 3.5ex; |
| 612 | 617 | max-width: 3.5ex; |
| @@ -1207,10 +1212,13 @@ | ||
| 1207 | 1212 | font-size: 1.2em; |
| 1208 | 1213 | padding: 0.2em; |
| 1209 | 1214 | margin: 0.25em 0; |
| 1210 | 1215 | flex: 0 0 auto; |
| 1211 | 1216 | } |
| 1217 | +.font-size-80 { | |
| 1218 | + font-size: 80%; | |
| 1219 | +} | |
| 1212 | 1220 | .font-size-100 { |
| 1213 | 1221 | font-size: 100%; |
| 1214 | 1222 | } |
| 1215 | 1223 | .font-size-125 { |
| 1216 | 1224 | font-size: 125%; |
| @@ -1301,16 +1309,17 @@ | ||
| 1301 | 1309 | padding: 0.25em 0 0 0 /*prevents slight overlap at top */; |
| 1302 | 1310 | } |
| 1303 | 1311 | table.numbered-lines td.line-numbers { |
| 1304 | 1312 | width: 4.5em; |
| 1305 | 1313 | } |
| 1306 | -table.numbered-lines td.line-numbers > span:first-of-type { | |
| 1307 | - margin-top: 0.25em/*must match top PADDING of | |
| 1308 | - td.file-content > pre > code*/; | |
| 1314 | +table.numbered-lines td.line-numbers > pre { | |
| 1315 | + margin: 0.25em/*must match top PADDING of td.file-content | |
| 1316 | + > pre > code*/ 0 0 0; | |
| 1317 | + padding: 0; | |
| 1309 | 1318 | } |
| 1310 | -table.numbered-lines td.line-numbers > span { | |
| 1311 | - display: block; | |
| 1319 | +table.numbered-lines td.line-numbers span { | |
| 1320 | + display: inline-block; | |
| 1312 | 1321 | margin: 0; |
| 1313 | 1322 | padding: 0; |
| 1314 | 1323 | line-height: inherit; |
| 1315 | 1324 | font-size: inherit; |
| 1316 | 1325 | font-family: inherit; |
| @@ -1317,11 +1326,11 @@ | ||
| 1317 | 1326 | cursor: pointer; |
| 1318 | 1327 | white-space: pre; |
| 1319 | 1328 | margin-right: 2px/*keep selection from nudging the right column */; |
| 1320 | 1329 | text-align: right; |
| 1321 | 1330 | } |
| 1322 | -table.numbered-lines td.line-numbers > span:hover { | |
| 1331 | +table.numbered-lines td.line-numbers span:hover { | |
| 1323 | 1332 | background-color: rgba(112, 112, 112, 0.25); |
| 1324 | 1333 | } |
| 1325 | 1334 | table.numbered-lines td.file-content { |
| 1326 | 1335 | padding-left: 0.25em; |
| 1327 | 1336 | } |
| @@ -1378,11 +1387,11 @@ | ||
| 1378 | 1387 | |
| 1379 | 1388 | .fossil-tooltip { |
| 1380 | 1389 | text-align: center; |
| 1381 | 1390 | padding: 0.2em 1em; |
| 1382 | 1391 | border: 1px solid black; |
| 1383 | - border-radius: 0.25em; | |
| 1392 | + border-radius: 0.5em; | |
| 1384 | 1393 | position: absolute; |
| 1385 | 1394 | display: inline-block; |
| 1386 | 1395 | z-index: 19/*below default skin's hamburger popup*/; |
| 1387 | 1396 | box-shadow: -0.15em 0.15em 0.2em rgba(0, 0, 0, 0.75); |
| 1388 | 1397 | background-color: inherit; |
| 1389 | 1398 |
| --- src/default.css | |
| +++ src/default.css | |
| @@ -602,11 +602,16 @@ | |
| 602 | tr.diffskip > td.chunkctrl { |
| 603 | text-align: left; |
| 604 | font-family: monospace; |
| 605 | } |
| 606 | tr.diffskip > td.chunkctrl > div { |
| 607 | /* Exists solely for layout purposes. */ |
| 608 | } |
| 609 | tr.diffskip > td.chunkctrl .jcbutton |
| 610 | /* class name .button breaks w/ some skins! */ { |
| 611 | min-width: 3.5ex; |
| 612 | max-width: 3.5ex; |
| @@ -1207,10 +1212,13 @@ | |
| 1207 | font-size: 1.2em; |
| 1208 | padding: 0.2em; |
| 1209 | margin: 0.25em 0; |
| 1210 | flex: 0 0 auto; |
| 1211 | } |
| 1212 | .font-size-100 { |
| 1213 | font-size: 100%; |
| 1214 | } |
| 1215 | .font-size-125 { |
| 1216 | font-size: 125%; |
| @@ -1301,16 +1309,17 @@ | |
| 1301 | padding: 0.25em 0 0 0 /*prevents slight overlap at top */; |
| 1302 | } |
| 1303 | table.numbered-lines td.line-numbers { |
| 1304 | width: 4.5em; |
| 1305 | } |
| 1306 | table.numbered-lines td.line-numbers > span:first-of-type { |
| 1307 | margin-top: 0.25em/*must match top PADDING of |
| 1308 | td.file-content > pre > code*/; |
| 1309 | } |
| 1310 | table.numbered-lines td.line-numbers > span { |
| 1311 | display: block; |
| 1312 | margin: 0; |
| 1313 | padding: 0; |
| 1314 | line-height: inherit; |
| 1315 | font-size: inherit; |
| 1316 | font-family: inherit; |
| @@ -1317,11 +1326,11 @@ | |
| 1317 | cursor: pointer; |
| 1318 | white-space: pre; |
| 1319 | margin-right: 2px/*keep selection from nudging the right column */; |
| 1320 | text-align: right; |
| 1321 | } |
| 1322 | table.numbered-lines td.line-numbers > span:hover { |
| 1323 | background-color: rgba(112, 112, 112, 0.25); |
| 1324 | } |
| 1325 | table.numbered-lines td.file-content { |
| 1326 | padding-left: 0.25em; |
| 1327 | } |
| @@ -1378,11 +1387,11 @@ | |
| 1378 | |
| 1379 | .fossil-tooltip { |
| 1380 | text-align: center; |
| 1381 | padding: 0.2em 1em; |
| 1382 | border: 1px solid black; |
| 1383 | border-radius: 0.25em; |
| 1384 | position: absolute; |
| 1385 | display: inline-block; |
| 1386 | z-index: 19/*below default skin's hamburger popup*/; |
| 1387 | box-shadow: -0.15em 0.15em 0.2em rgba(0, 0, 0, 0.75); |
| 1388 | background-color: inherit; |
| 1389 |
| --- src/default.css | |
| +++ src/default.css | |
| @@ -602,11 +602,16 @@ | |
| 602 | tr.diffskip > td.chunkctrl { |
| 603 | text-align: left; |
| 604 | font-family: monospace; |
| 605 | } |
| 606 | tr.diffskip > td.chunkctrl > div { |
| 607 | display: flex; |
| 608 | align-items: center; |
| 609 | } |
| 610 | tr.diffskip > td.chunkctrl > div > span.error { |
| 611 | padding: 0.25em 0.5em; |
| 612 | border-radius: 0.5em; |
| 613 | } |
| 614 | tr.diffskip > td.chunkctrl .jcbutton |
| 615 | /* class name .button breaks w/ some skins! */ { |
| 616 | min-width: 3.5ex; |
| 617 | max-width: 3.5ex; |
| @@ -1207,10 +1212,13 @@ | |
| 1212 | font-size: 1.2em; |
| 1213 | padding: 0.2em; |
| 1214 | margin: 0.25em 0; |
| 1215 | flex: 0 0 auto; |
| 1216 | } |
| 1217 | .font-size-80 { |
| 1218 | font-size: 80%; |
| 1219 | } |
| 1220 | .font-size-100 { |
| 1221 | font-size: 100%; |
| 1222 | } |
| 1223 | .font-size-125 { |
| 1224 | font-size: 125%; |
| @@ -1301,16 +1309,17 @@ | |
| 1309 | padding: 0.25em 0 0 0 /*prevents slight overlap at top */; |
| 1310 | } |
| 1311 | table.numbered-lines td.line-numbers { |
| 1312 | width: 4.5em; |
| 1313 | } |
| 1314 | table.numbered-lines td.line-numbers > pre { |
| 1315 | margin: 0.25em/*must match top PADDING of td.file-content |
| 1316 | > pre > code*/ 0 0 0; |
| 1317 | padding: 0; |
| 1318 | } |
| 1319 | table.numbered-lines td.line-numbers span { |
| 1320 | display: inline-block; |
| 1321 | margin: 0; |
| 1322 | padding: 0; |
| 1323 | line-height: inherit; |
| 1324 | font-size: inherit; |
| 1325 | font-family: inherit; |
| @@ -1317,11 +1326,11 @@ | |
| 1326 | cursor: pointer; |
| 1327 | white-space: pre; |
| 1328 | margin-right: 2px/*keep selection from nudging the right column */; |
| 1329 | text-align: right; |
| 1330 | } |
| 1331 | table.numbered-lines td.line-numbers span:hover { |
| 1332 | background-color: rgba(112, 112, 112, 0.25); |
| 1333 | } |
| 1334 | table.numbered-lines td.file-content { |
| 1335 | padding-left: 0.25em; |
| 1336 | } |
| @@ -1378,11 +1387,11 @@ | |
| 1387 | |
| 1388 | .fossil-tooltip { |
| 1389 | text-align: center; |
| 1390 | padding: 0.2em 1em; |
| 1391 | border: 1px solid black; |
| 1392 | border-radius: 0.5em; |
| 1393 | position: absolute; |
| 1394 | display: inline-block; |
| 1395 | z-index: 19/*below default skin's hamburger popup*/; |
| 1396 | box-shadow: -0.15em 0.15em 0.2em rgba(0, 0, 0, 0.75); |
| 1397 | background-color: inherit; |
| 1398 |
+33
-7
| --- src/fossil.diff.js | ||
| +++ src/fossil.diff.js | ||
| @@ -33,11 +33,11 @@ | ||
| 33 | 33 | /* Default callack handlers for Diff.fetchArtifactChunk(), |
| 34 | 34 | unless overridden by options passeed to that function. */ |
| 35 | 35 | beforesend: function(){}, |
| 36 | 36 | aftersend: function(){}, |
| 37 | 37 | onerror: function(e){ |
| 38 | - F.toast.error("XHR error: ",e.message); | |
| 38 | + console.error("XHR error: ",e); | |
| 39 | 39 | } |
| 40 | 40 | } |
| 41 | 41 | } |
| 42 | 42 | }; |
| 43 | 43 | /** |
| @@ -138,10 +138,11 @@ | ||
| 138 | 138 | this.e.td = D.addClass( |
| 139 | 139 | /* Holder for our UI controls */ |
| 140 | 140 | D.attr(D.td(tr), 'colspan', this.isSplit ? 5 : 4), |
| 141 | 141 | 'chunkctrl' |
| 142 | 142 | ); |
| 143 | + this.e.msgWidget = D.addClass(D.span(), 'hidden'); | |
| 143 | 144 | this.e.btnWrapper = D.div(); |
| 144 | 145 | D.append(this.e.td, this.e.btnWrapper); |
| 145 | 146 | /** |
| 146 | 147 | Depending on various factors, we need one or more of: |
| 147 | 148 | |
| @@ -188,10 +189,11 @@ | ||
| 188 | 189 | } |
| 189 | 190 | //this.e.btnUp = btnUp; |
| 190 | 191 | //this.e.btnDown = btnDown; |
| 191 | 192 | if(btnDown) D.append(this.e.btnWrapper, btnDown); |
| 192 | 193 | if(btnUp) D.append(this.e.btnWrapper, btnUp); |
| 194 | + D.append(this.e.btnWrapper, this.e.msgWidget); | |
| 193 | 195 | /* For debugging only... */ |
| 194 | 196 | this.e.posState = D.span(); |
| 195 | 197 | D.append(this.e.btnWrapper, this.e.posState); |
| 196 | 198 | this.updatePosDebug(); |
| 197 | 199 | }; |
| @@ -295,10 +297,11 @@ | ||
| 295 | 297 | if(!lines.length){ |
| 296 | 298 | /* No more data to load */ |
| 297 | 299 | this.destroy(); |
| 298 | 300 | return this; |
| 299 | 301 | } |
| 302 | + this.msg(false); | |
| 300 | 303 | //console.debug("Loaded line range ", |
| 301 | 304 | //urlParam.from,"-",urlParam.to, "fetchType ",fetchType); |
| 302 | 305 | const lineno = [], |
| 303 | 306 | trPrev = this.e.tr.previousElementSibling, |
| 304 | 307 | trNext = this.e.tr.nextElementSibling, |
| @@ -311,14 +314,18 @@ | ||
| 311 | 314 | ) ? trNext : false |
| 312 | 315 | /* Truthy if we want to combine trPrev, the new content, and |
| 313 | 316 | trNext into trPrev and then remove trNext. */; |
| 314 | 317 | let i, td; |
| 315 | 318 | if(!f.convertLines){ |
| 319 | + /* Reminder: string.replaceAll() is a relatively new | |
| 320 | + JS feature, not available in some still-widely-used | |
| 321 | + browser versions. */ | |
| 322 | + f.rx = [[/&/g, '&'], [/</g, '<']]; | |
| 316 | 323 | f.convertLines = function(li){ |
| 317 | - return li.join('\n') | |
| 318 | - .replaceAll('&','&') | |
| 319 | - .replaceAll('<','<')+'\n'; | |
| 324 | + var s = li.join('\n'); | |
| 325 | + f.rx.forEach((a)=>s=s.replace(a[0],a[1])); | |
| 326 | + return s + '\n'; | |
| 320 | 327 | }; |
| 321 | 328 | } |
| 322 | 329 | if(1){ // LHS line numbers... |
| 323 | 330 | const selector = '.difflnl > pre'; |
| 324 | 331 | td = tr.querySelector(selector); |
| @@ -340,11 +347,11 @@ | ||
| 340 | 347 | |
| 341 | 348 | if(1){// code block(s)... |
| 342 | 349 | const selector = '.difftxt > pre'; |
| 343 | 350 | td = tr.querySelectorAll(selector); |
| 344 | 351 | const code = f.convertLines(lines); |
| 345 | - let joinNdx = 0; | |
| 352 | + let joinNdx = 0/*selector[X] index to join together*/; | |
| 346 | 353 | td.forEach(function(e){ |
| 347 | 354 | const content = [e.innerHTML]; |
| 348 | 355 | if(doAppend) content.push(code); |
| 349 | 356 | else content.unshift(code); |
| 350 | 357 | if(joinTr){ |
| @@ -454,10 +461,28 @@ | ||
| 454 | 461 | return this; |
| 455 | 462 | }else{ |
| 456 | 463 | throw new Error("Unexpected 'fetchType' value."); |
| 457 | 464 | } |
| 458 | 465 | }, |
| 466 | + | |
| 467 | + /** | |
| 468 | + Sets this widget's message to the given text. If the message | |
| 469 | + represents an error, the first argument must be truthy, else it | |
| 470 | + must be falsy. Returns this object. | |
| 471 | + */ | |
| 472 | + msg: function(isError,txt){ | |
| 473 | + if(txt){ | |
| 474 | + if(isError) D.addClass(this.e.msgWidget, 'error'); | |
| 475 | + else D.removeClass(this.e.msgWidget, 'error'); | |
| 476 | + D.append( | |
| 477 | + D.removeClass(D.clearElement(this.e.msgWidget), 'hidden'), | |
| 478 | + txt); | |
| 479 | + }else{ | |
| 480 | + D.addClass(D.clearElement(this.e.msgWidget), 'hidden'); | |
| 481 | + } | |
| 482 | + return this; | |
| 483 | + }, | |
| 459 | 484 | |
| 460 | 485 | /** |
| 461 | 486 | Fetches and inserts a line chunk. fetchType is: |
| 462 | 487 | |
| 463 | 488 | this.FetchType.NextUp = upwards from next chunk (this.pos.next) |
| @@ -480,18 +505,18 @@ | ||
| 480 | 505 | fetchChunk: function(fetchType){ |
| 481 | 506 | /* Forewarning, this is a bit confusing: when fetching the |
| 482 | 507 | previous lines, we're doing so on behalf of the *next* diff |
| 483 | 508 | chunk (this.pos.next), and vice versa. */ |
| 484 | 509 | if(this.$isFetching){ |
| 485 | - F.toast.warning("Cannot load chunk while a load is pending."); | |
| 486 | - return this; | |
| 510 | + return this.msg(true,"Cannot load chunk while a load is pending."); | |
| 487 | 511 | } |
| 488 | 512 | if(fetchType===this.FetchType.NextUp && !this.pos.next |
| 489 | 513 | || fetchType===this.FetchType.PrevDown && !this.pos.prev){ |
| 490 | 514 | console.error("Attempt to fetch diff lines but don't have any."); |
| 491 | 515 | return this; |
| 492 | 516 | } |
| 517 | + this.msg(false,"Fetching diff chunk..."); | |
| 493 | 518 | const fOpt = { |
| 494 | 519 | urlParams:{ |
| 495 | 520 | name: this.fileHash, from: 0, to: 0 |
| 496 | 521 | }, |
| 497 | 522 | aftersend: ()=>delete this.$isFetching, |
| @@ -528,10 +553,11 @@ | ||
| 528 | 553 | fetchType = this.FetchType.FillGap; |
| 529 | 554 | } |
| 530 | 555 | } |
| 531 | 556 | this.$isFetching = true; |
| 532 | 557 | //console.debug("fetchChunk(",fetchType,")",up); |
| 558 | + fOpt.onerror = (err)=>this.msg(true,err.message); | |
| 533 | 559 | Diff.fetchArtifactChunk(fOpt); |
| 534 | 560 | return this; |
| 535 | 561 | } |
| 536 | 562 | }; |
| 537 | 563 | |
| 538 | 564 |
| --- src/fossil.diff.js | |
| +++ src/fossil.diff.js | |
| @@ -33,11 +33,11 @@ | |
| 33 | /* Default callack handlers for Diff.fetchArtifactChunk(), |
| 34 | unless overridden by options passeed to that function. */ |
| 35 | beforesend: function(){}, |
| 36 | aftersend: function(){}, |
| 37 | onerror: function(e){ |
| 38 | F.toast.error("XHR error: ",e.message); |
| 39 | } |
| 40 | } |
| 41 | } |
| 42 | }; |
| 43 | /** |
| @@ -138,10 +138,11 @@ | |
| 138 | this.e.td = D.addClass( |
| 139 | /* Holder for our UI controls */ |
| 140 | D.attr(D.td(tr), 'colspan', this.isSplit ? 5 : 4), |
| 141 | 'chunkctrl' |
| 142 | ); |
| 143 | this.e.btnWrapper = D.div(); |
| 144 | D.append(this.e.td, this.e.btnWrapper); |
| 145 | /** |
| 146 | Depending on various factors, we need one or more of: |
| 147 | |
| @@ -188,10 +189,11 @@ | |
| 188 | } |
| 189 | //this.e.btnUp = btnUp; |
| 190 | //this.e.btnDown = btnDown; |
| 191 | if(btnDown) D.append(this.e.btnWrapper, btnDown); |
| 192 | if(btnUp) D.append(this.e.btnWrapper, btnUp); |
| 193 | /* For debugging only... */ |
| 194 | this.e.posState = D.span(); |
| 195 | D.append(this.e.btnWrapper, this.e.posState); |
| 196 | this.updatePosDebug(); |
| 197 | }; |
| @@ -295,10 +297,11 @@ | |
| 295 | if(!lines.length){ |
| 296 | /* No more data to load */ |
| 297 | this.destroy(); |
| 298 | return this; |
| 299 | } |
| 300 | //console.debug("Loaded line range ", |
| 301 | //urlParam.from,"-",urlParam.to, "fetchType ",fetchType); |
| 302 | const lineno = [], |
| 303 | trPrev = this.e.tr.previousElementSibling, |
| 304 | trNext = this.e.tr.nextElementSibling, |
| @@ -311,14 +314,18 @@ | |
| 311 | ) ? trNext : false |
| 312 | /* Truthy if we want to combine trPrev, the new content, and |
| 313 | trNext into trPrev and then remove trNext. */; |
| 314 | let i, td; |
| 315 | if(!f.convertLines){ |
| 316 | f.convertLines = function(li){ |
| 317 | return li.join('\n') |
| 318 | .replaceAll('&','&') |
| 319 | .replaceAll('<','<')+'\n'; |
| 320 | }; |
| 321 | } |
| 322 | if(1){ // LHS line numbers... |
| 323 | const selector = '.difflnl > pre'; |
| 324 | td = tr.querySelector(selector); |
| @@ -340,11 +347,11 @@ | |
| 340 | |
| 341 | if(1){// code block(s)... |
| 342 | const selector = '.difftxt > pre'; |
| 343 | td = tr.querySelectorAll(selector); |
| 344 | const code = f.convertLines(lines); |
| 345 | let joinNdx = 0; |
| 346 | td.forEach(function(e){ |
| 347 | const content = [e.innerHTML]; |
| 348 | if(doAppend) content.push(code); |
| 349 | else content.unshift(code); |
| 350 | if(joinTr){ |
| @@ -454,10 +461,28 @@ | |
| 454 | return this; |
| 455 | }else{ |
| 456 | throw new Error("Unexpected 'fetchType' value."); |
| 457 | } |
| 458 | }, |
| 459 | |
| 460 | /** |
| 461 | Fetches and inserts a line chunk. fetchType is: |
| 462 | |
| 463 | this.FetchType.NextUp = upwards from next chunk (this.pos.next) |
| @@ -480,18 +505,18 @@ | |
| 480 | fetchChunk: function(fetchType){ |
| 481 | /* Forewarning, this is a bit confusing: when fetching the |
| 482 | previous lines, we're doing so on behalf of the *next* diff |
| 483 | chunk (this.pos.next), and vice versa. */ |
| 484 | if(this.$isFetching){ |
| 485 | F.toast.warning("Cannot load chunk while a load is pending."); |
| 486 | return this; |
| 487 | } |
| 488 | if(fetchType===this.FetchType.NextUp && !this.pos.next |
| 489 | || fetchType===this.FetchType.PrevDown && !this.pos.prev){ |
| 490 | console.error("Attempt to fetch diff lines but don't have any."); |
| 491 | return this; |
| 492 | } |
| 493 | const fOpt = { |
| 494 | urlParams:{ |
| 495 | name: this.fileHash, from: 0, to: 0 |
| 496 | }, |
| 497 | aftersend: ()=>delete this.$isFetching, |
| @@ -528,10 +553,11 @@ | |
| 528 | fetchType = this.FetchType.FillGap; |
| 529 | } |
| 530 | } |
| 531 | this.$isFetching = true; |
| 532 | //console.debug("fetchChunk(",fetchType,")",up); |
| 533 | Diff.fetchArtifactChunk(fOpt); |
| 534 | return this; |
| 535 | } |
| 536 | }; |
| 537 | |
| 538 |
| --- src/fossil.diff.js | |
| +++ src/fossil.diff.js | |
| @@ -33,11 +33,11 @@ | |
| 33 | /* Default callack handlers for Diff.fetchArtifactChunk(), |
| 34 | unless overridden by options passeed to that function. */ |
| 35 | beforesend: function(){}, |
| 36 | aftersend: function(){}, |
| 37 | onerror: function(e){ |
| 38 | console.error("XHR error: ",e); |
| 39 | } |
| 40 | } |
| 41 | } |
| 42 | }; |
| 43 | /** |
| @@ -138,10 +138,11 @@ | |
| 138 | this.e.td = D.addClass( |
| 139 | /* Holder for our UI controls */ |
| 140 | D.attr(D.td(tr), 'colspan', this.isSplit ? 5 : 4), |
| 141 | 'chunkctrl' |
| 142 | ); |
| 143 | this.e.msgWidget = D.addClass(D.span(), 'hidden'); |
| 144 | this.e.btnWrapper = D.div(); |
| 145 | D.append(this.e.td, this.e.btnWrapper); |
| 146 | /** |
| 147 | Depending on various factors, we need one or more of: |
| 148 | |
| @@ -188,10 +189,11 @@ | |
| 189 | } |
| 190 | //this.e.btnUp = btnUp; |
| 191 | //this.e.btnDown = btnDown; |
| 192 | if(btnDown) D.append(this.e.btnWrapper, btnDown); |
| 193 | if(btnUp) D.append(this.e.btnWrapper, btnUp); |
| 194 | D.append(this.e.btnWrapper, this.e.msgWidget); |
| 195 | /* For debugging only... */ |
| 196 | this.e.posState = D.span(); |
| 197 | D.append(this.e.btnWrapper, this.e.posState); |
| 198 | this.updatePosDebug(); |
| 199 | }; |
| @@ -295,10 +297,11 @@ | |
| 297 | if(!lines.length){ |
| 298 | /* No more data to load */ |
| 299 | this.destroy(); |
| 300 | return this; |
| 301 | } |
| 302 | this.msg(false); |
| 303 | //console.debug("Loaded line range ", |
| 304 | //urlParam.from,"-",urlParam.to, "fetchType ",fetchType); |
| 305 | const lineno = [], |
| 306 | trPrev = this.e.tr.previousElementSibling, |
| 307 | trNext = this.e.tr.nextElementSibling, |
| @@ -311,14 +314,18 @@ | |
| 314 | ) ? trNext : false |
| 315 | /* Truthy if we want to combine trPrev, the new content, and |
| 316 | trNext into trPrev and then remove trNext. */; |
| 317 | let i, td; |
| 318 | if(!f.convertLines){ |
| 319 | /* Reminder: string.replaceAll() is a relatively new |
| 320 | JS feature, not available in some still-widely-used |
| 321 | browser versions. */ |
| 322 | f.rx = [[/&/g, '&'], [/</g, '<']]; |
| 323 | f.convertLines = function(li){ |
| 324 | var s = li.join('\n'); |
| 325 | f.rx.forEach((a)=>s=s.replace(a[0],a[1])); |
| 326 | return s + '\n'; |
| 327 | }; |
| 328 | } |
| 329 | if(1){ // LHS line numbers... |
| 330 | const selector = '.difflnl > pre'; |
| 331 | td = tr.querySelector(selector); |
| @@ -340,11 +347,11 @@ | |
| 347 | |
| 348 | if(1){// code block(s)... |
| 349 | const selector = '.difftxt > pre'; |
| 350 | td = tr.querySelectorAll(selector); |
| 351 | const code = f.convertLines(lines); |
| 352 | let joinNdx = 0/*selector[X] index to join together*/; |
| 353 | td.forEach(function(e){ |
| 354 | const content = [e.innerHTML]; |
| 355 | if(doAppend) content.push(code); |
| 356 | else content.unshift(code); |
| 357 | if(joinTr){ |
| @@ -454,10 +461,28 @@ | |
| 461 | return this; |
| 462 | }else{ |
| 463 | throw new Error("Unexpected 'fetchType' value."); |
| 464 | } |
| 465 | }, |
| 466 | |
| 467 | /** |
| 468 | Sets this widget's message to the given text. If the message |
| 469 | represents an error, the first argument must be truthy, else it |
| 470 | must be falsy. Returns this object. |
| 471 | */ |
| 472 | msg: function(isError,txt){ |
| 473 | if(txt){ |
| 474 | if(isError) D.addClass(this.e.msgWidget, 'error'); |
| 475 | else D.removeClass(this.e.msgWidget, 'error'); |
| 476 | D.append( |
| 477 | D.removeClass(D.clearElement(this.e.msgWidget), 'hidden'), |
| 478 | txt); |
| 479 | }else{ |
| 480 | D.addClass(D.clearElement(this.e.msgWidget), 'hidden'); |
| 481 | } |
| 482 | return this; |
| 483 | }, |
| 484 | |
| 485 | /** |
| 486 | Fetches and inserts a line chunk. fetchType is: |
| 487 | |
| 488 | this.FetchType.NextUp = upwards from next chunk (this.pos.next) |
| @@ -480,18 +505,18 @@ | |
| 505 | fetchChunk: function(fetchType){ |
| 506 | /* Forewarning, this is a bit confusing: when fetching the |
| 507 | previous lines, we're doing so on behalf of the *next* diff |
| 508 | chunk (this.pos.next), and vice versa. */ |
| 509 | if(this.$isFetching){ |
| 510 | return this.msg(true,"Cannot load chunk while a load is pending."); |
| 511 | } |
| 512 | if(fetchType===this.FetchType.NextUp && !this.pos.next |
| 513 | || fetchType===this.FetchType.PrevDown && !this.pos.prev){ |
| 514 | console.error("Attempt to fetch diff lines but don't have any."); |
| 515 | return this; |
| 516 | } |
| 517 | this.msg(false,"Fetching diff chunk..."); |
| 518 | const fOpt = { |
| 519 | urlParams:{ |
| 520 | name: this.fileHash, from: 0, to: 0 |
| 521 | }, |
| 522 | aftersend: ()=>delete this.$isFetching, |
| @@ -528,10 +553,11 @@ | |
| 553 | fetchType = this.FetchType.FillGap; |
| 554 | } |
| 555 | } |
| 556 | this.$isFetching = true; |
| 557 | //console.debug("fetchChunk(",fetchType,")",up); |
| 558 | fOpt.onerror = (err)=>this.msg(true,err.message); |
| 559 | Diff.fetchArtifactChunk(fOpt); |
| 560 | return this; |
| 561 | } |
| 562 | }; |
| 563 | |
| 564 |
+76
-1
| --- src/fossil.dom.js | ||
| +++ src/fossil.dom.js | ||
| @@ -119,13 +119,18 @@ | ||
| 119 | 119 | /** Returns a new TEXT node which contains the text of all of the |
| 120 | 120 | arguments appended together. */ |
| 121 | 121 | dom.text = function(/*...*/){ |
| 122 | 122 | return document.createTextNode(argsToArray(arguments).join('')); |
| 123 | 123 | }; |
| 124 | - dom.button = function(label){ | |
| 124 | + /** Returns a new Button element with the given optional | |
| 125 | + label and on-click event listener function. */ | |
| 126 | + dom.button = function(label,callback){ | |
| 125 | 127 | const b = this.create('button'); |
| 126 | 128 | if(label) b.appendChild(this.text(label)); |
| 129 | + if('function' === typeof callback){ | |
| 130 | + b.addEventListener('click', callback, false); | |
| 131 | + } | |
| 127 | 132 | return b; |
| 128 | 133 | }; |
| 129 | 134 | /** |
| 130 | 135 | Returns a TEXTAREA element. |
| 131 | 136 | |
| @@ -677,10 +682,80 @@ | ||
| 677 | 682 | A DOM event handler which simply passes event.target |
| 678 | 683 | to dom.flashOnce(). |
| 679 | 684 | */ |
| 680 | 685 | dom.flashOnce.eventHandler = (event)=>dom.flashOnce(event.target) |
| 681 | 686 | |
| 687 | + /** | |
| 688 | + This variant of flashOnce() flashes the element e n times | |
| 689 | + for a duration of howLongMs milliseconds then calls the | |
| 690 | + afterFlashCallback() callback. It may also be called with 2 | |
| 691 | + or 3 arguments, in which case: | |
| 692 | + | |
| 693 | + 2 arguments: default flash time and no callback. | |
| 694 | + | |
| 695 | + 3 arguments: 3rd may be a flash delay time or a callback | |
| 696 | + function. | |
| 697 | + | |
| 698 | + Returns this object but the flashing is asynchronous. | |
| 699 | + | |
| 700 | + Depending on system load and related factors, a multi-flash | |
| 701 | + animation might stutter and look suboptimal. | |
| 702 | + */ | |
| 703 | + dom.flashNTimes = function(e,n,howLongMs,afterFlashCallback){ | |
| 704 | + const args = argsToArray(arguments); | |
| 705 | + args.splice(1,1); | |
| 706 | + if(arguments.length===3 && 'function'===typeof howLongMs){ | |
| 707 | + afterFlashCallback = howLongMs; | |
| 708 | + howLongMs = args[1] = this.flashOnce.defaultTimeMs; | |
| 709 | + }else if(arguments.length<3){ | |
| 710 | + args[1] = this.flashOnce.defaultTimeMs; | |
| 711 | + } | |
| 712 | + n = +n; | |
| 713 | + const self = this; | |
| 714 | + const cb = args[2] = function f(){ | |
| 715 | + if(--n){ | |
| 716 | + setTimeout(()=>self.flashOnce(e, howLongMs, f), | |
| 717 | + howLongMs+(howLongMs*0.1)/*we need a slight gap here*/); | |
| 718 | + }else if(afterFlashCallback){ | |
| 719 | + afterFlashCallback(); | |
| 720 | + } | |
| 721 | + }; | |
| 722 | + this.flashOnce.apply(this, args); | |
| 723 | + return this; | |
| 724 | + }; | |
| 725 | + | |
| 726 | + /** | |
| 727 | + Adds the given CSS class or array of CSS classes to the given | |
| 728 | + element or forEach-capable list of elements for howLongMs, then | |
| 729 | + removes it. If afterCallack is a function, it is called after the | |
| 730 | + CSS class is removed from all elements. If called with 3 | |
| 731 | + arguments and the 3rd is a function, the 3rd is treated as a | |
| 732 | + callback and the default time (addClassBriefly.defaultTimeMs) is | |
| 733 | + used. If called with only 2 arguments, a time of | |
| 734 | + addClassBriefly.defaultTimeMs is used. | |
| 735 | + | |
| 736 | + Passing a value of 0 for howLongMs causes the default value | |
| 737 | + to be applied. | |
| 738 | + | |
| 739 | + Returns this object but the CSS removal is asynchronous. | |
| 740 | + */ | |
| 741 | + dom.addClassBriefly = function f(e, className, howLongMs, afterCallback){ | |
| 742 | + if(arguments.length<4 && 'function'===typeof howLongMs){ | |
| 743 | + afterCallback = howLongMs; | |
| 744 | + howLongMs = f.defaultTimeMs; | |
| 745 | + }else if(arguments.length<3 || !+howLongMs){ | |
| 746 | + howLongMs = f.defaultTimeMs; | |
| 747 | + } | |
| 748 | + this.addClass(e, className); | |
| 749 | + setTimeout(function(){ | |
| 750 | + dom.removeClass(e, className); | |
| 751 | + if(afterCallback) afterCallback(); | |
| 752 | + }, howLongMs); | |
| 753 | + return this; | |
| 754 | + }; | |
| 755 | + dom.addClassBriefly.defaultTimeMs = 1000; | |
| 756 | + | |
| 682 | 757 | /** |
| 683 | 758 | Attempts to copy the given text to the system clipboard. Returns |
| 684 | 759 | true if it succeeds, else false. |
| 685 | 760 | */ |
| 686 | 761 | dom.copyTextToClipboard = function(text){ |
| 687 | 762 |
| --- src/fossil.dom.js | |
| +++ src/fossil.dom.js | |
| @@ -119,13 +119,18 @@ | |
| 119 | /** Returns a new TEXT node which contains the text of all of the |
| 120 | arguments appended together. */ |
| 121 | dom.text = function(/*...*/){ |
| 122 | return document.createTextNode(argsToArray(arguments).join('')); |
| 123 | }; |
| 124 | dom.button = function(label){ |
| 125 | const b = this.create('button'); |
| 126 | if(label) b.appendChild(this.text(label)); |
| 127 | return b; |
| 128 | }; |
| 129 | /** |
| 130 | Returns a TEXTAREA element. |
| 131 | |
| @@ -677,10 +682,80 @@ | |
| 677 | A DOM event handler which simply passes event.target |
| 678 | to dom.flashOnce(). |
| 679 | */ |
| 680 | dom.flashOnce.eventHandler = (event)=>dom.flashOnce(event.target) |
| 681 | |
| 682 | /** |
| 683 | Attempts to copy the given text to the system clipboard. Returns |
| 684 | true if it succeeds, else false. |
| 685 | */ |
| 686 | dom.copyTextToClipboard = function(text){ |
| 687 |
| --- src/fossil.dom.js | |
| +++ src/fossil.dom.js | |
| @@ -119,13 +119,18 @@ | |
| 119 | /** Returns a new TEXT node which contains the text of all of the |
| 120 | arguments appended together. */ |
| 121 | dom.text = function(/*...*/){ |
| 122 | return document.createTextNode(argsToArray(arguments).join('')); |
| 123 | }; |
| 124 | /** Returns a new Button element with the given optional |
| 125 | label and on-click event listener function. */ |
| 126 | dom.button = function(label,callback){ |
| 127 | const b = this.create('button'); |
| 128 | if(label) b.appendChild(this.text(label)); |
| 129 | if('function' === typeof callback){ |
| 130 | b.addEventListener('click', callback, false); |
| 131 | } |
| 132 | return b; |
| 133 | }; |
| 134 | /** |
| 135 | Returns a TEXTAREA element. |
| 136 | |
| @@ -677,10 +682,80 @@ | |
| 682 | A DOM event handler which simply passes event.target |
| 683 | to dom.flashOnce(). |
| 684 | */ |
| 685 | dom.flashOnce.eventHandler = (event)=>dom.flashOnce(event.target) |
| 686 | |
| 687 | /** |
| 688 | This variant of flashOnce() flashes the element e n times |
| 689 | for a duration of howLongMs milliseconds then calls the |
| 690 | afterFlashCallback() callback. It may also be called with 2 |
| 691 | or 3 arguments, in which case: |
| 692 | |
| 693 | 2 arguments: default flash time and no callback. |
| 694 | |
| 695 | 3 arguments: 3rd may be a flash delay time or a callback |
| 696 | function. |
| 697 | |
| 698 | Returns this object but the flashing is asynchronous. |
| 699 | |
| 700 | Depending on system load and related factors, a multi-flash |
| 701 | animation might stutter and look suboptimal. |
| 702 | */ |
| 703 | dom.flashNTimes = function(e,n,howLongMs,afterFlashCallback){ |
| 704 | const args = argsToArray(arguments); |
| 705 | args.splice(1,1); |
| 706 | if(arguments.length===3 && 'function'===typeof howLongMs){ |
| 707 | afterFlashCallback = howLongMs; |
| 708 | howLongMs = args[1] = this.flashOnce.defaultTimeMs; |
| 709 | }else if(arguments.length<3){ |
| 710 | args[1] = this.flashOnce.defaultTimeMs; |
| 711 | } |
| 712 | n = +n; |
| 713 | const self = this; |
| 714 | const cb = args[2] = function f(){ |
| 715 | if(--n){ |
| 716 | setTimeout(()=>self.flashOnce(e, howLongMs, f), |
| 717 | howLongMs+(howLongMs*0.1)/*we need a slight gap here*/); |
| 718 | }else if(afterFlashCallback){ |
| 719 | afterFlashCallback(); |
| 720 | } |
| 721 | }; |
| 722 | this.flashOnce.apply(this, args); |
| 723 | return this; |
| 724 | }; |
| 725 | |
| 726 | /** |
| 727 | Adds the given CSS class or array of CSS classes to the given |
| 728 | element or forEach-capable list of elements for howLongMs, then |
| 729 | removes it. If afterCallack is a function, it is called after the |
| 730 | CSS class is removed from all elements. If called with 3 |
| 731 | arguments and the 3rd is a function, the 3rd is treated as a |
| 732 | callback and the default time (addClassBriefly.defaultTimeMs) is |
| 733 | used. If called with only 2 arguments, a time of |
| 734 | addClassBriefly.defaultTimeMs is used. |
| 735 | |
| 736 | Passing a value of 0 for howLongMs causes the default value |
| 737 | to be applied. |
| 738 | |
| 739 | Returns this object but the CSS removal is asynchronous. |
| 740 | */ |
| 741 | dom.addClassBriefly = function f(e, className, howLongMs, afterCallback){ |
| 742 | if(arguments.length<4 && 'function'===typeof howLongMs){ |
| 743 | afterCallback = howLongMs; |
| 744 | howLongMs = f.defaultTimeMs; |
| 745 | }else if(arguments.length<3 || !+howLongMs){ |
| 746 | howLongMs = f.defaultTimeMs; |
| 747 | } |
| 748 | this.addClass(e, className); |
| 749 | setTimeout(function(){ |
| 750 | dom.removeClass(e, className); |
| 751 | if(afterCallback) afterCallback(); |
| 752 | }, howLongMs); |
| 753 | return this; |
| 754 | }; |
| 755 | dom.addClassBriefly.defaultTimeMs = 1000; |
| 756 | |
| 757 | /** |
| 758 | Attempts to copy the given text to the system clipboard. Returns |
| 759 | true if it succeeds, else false. |
| 760 | */ |
| 761 | dom.copyTextToClipboard = function(text){ |
| 762 |
| --- src/fossil.popupwidget.js | ||
| +++ src/fossil.popupwidget.js | ||
| @@ -355,10 +355,11 @@ | ||
| 355 | 355 | */ |
| 356 | 356 | setup: function f(){ |
| 357 | 357 | if(!f.hasOwnProperty('clickHandler')){ |
| 358 | 358 | f.clickHandler = function fch(ev){ |
| 359 | 359 | ev.preventDefault(); |
| 360 | + ev.stopPropagation(); | |
| 360 | 361 | if(!fch.popup){ |
| 361 | 362 | fch.popup = new F.PopupWidget({ |
| 362 | 363 | cssClass: ['fossil-tooltip', 'help-buttonlet-content'], |
| 363 | 364 | refresh: function(){ |
| 364 | 365 | } |
| @@ -411,10 +412,11 @@ | ||
| 411 | 412 | x -= popupRect.width/2; |
| 412 | 413 | } |
| 413 | 414 | if(x<0) x = 0; |
| 414 | 415 | //console.debug("dimensions",x,y, popupRect, rectBody); |
| 415 | 416 | fch.popup.show(x, y); |
| 417 | + return false; | |
| 416 | 418 | }; |
| 417 | 419 | f.foreachElement = function(e){ |
| 418 | 420 | if(e.classList.contains('processed')) return; |
| 419 | 421 | e.classList.add('processed'); |
| 420 | 422 | e.$helpContent = []; |
| 421 | 423 |
| --- src/fossil.popupwidget.js | |
| +++ src/fossil.popupwidget.js | |
| @@ -355,10 +355,11 @@ | |
| 355 | */ |
| 356 | setup: function f(){ |
| 357 | if(!f.hasOwnProperty('clickHandler')){ |
| 358 | f.clickHandler = function fch(ev){ |
| 359 | ev.preventDefault(); |
| 360 | if(!fch.popup){ |
| 361 | fch.popup = new F.PopupWidget({ |
| 362 | cssClass: ['fossil-tooltip', 'help-buttonlet-content'], |
| 363 | refresh: function(){ |
| 364 | } |
| @@ -411,10 +412,11 @@ | |
| 411 | x -= popupRect.width/2; |
| 412 | } |
| 413 | if(x<0) x = 0; |
| 414 | //console.debug("dimensions",x,y, popupRect, rectBody); |
| 415 | fch.popup.show(x, y); |
| 416 | }; |
| 417 | f.foreachElement = function(e){ |
| 418 | if(e.classList.contains('processed')) return; |
| 419 | e.classList.add('processed'); |
| 420 | e.$helpContent = []; |
| 421 |
| --- src/fossil.popupwidget.js | |
| +++ src/fossil.popupwidget.js | |
| @@ -355,10 +355,11 @@ | |
| 355 | */ |
| 356 | setup: function f(){ |
| 357 | if(!f.hasOwnProperty('clickHandler')){ |
| 358 | f.clickHandler = function fch(ev){ |
| 359 | ev.preventDefault(); |
| 360 | ev.stopPropagation(); |
| 361 | if(!fch.popup){ |
| 362 | fch.popup = new F.PopupWidget({ |
| 363 | cssClass: ['fossil-tooltip', 'help-buttonlet-content'], |
| 364 | refresh: function(){ |
| 365 | } |
| @@ -411,10 +412,11 @@ | |
| 412 | x -= popupRect.width/2; |
| 413 | } |
| 414 | if(x<0) x = 0; |
| 415 | //console.debug("dimensions",x,y, popupRect, rectBody); |
| 416 | fch.popup.show(x, y); |
| 417 | return false; |
| 418 | }; |
| 419 | f.foreachElement = function(e){ |
| 420 | if(e.classList.contains('processed')) return; |
| 421 | e.classList.add('processed'); |
| 422 | e.$helpContent = []; |
| 423 |
+1
-1
| --- src/fossil.storage.js | ||
| +++ src/fossil.storage.js | ||
| @@ -1,8 +1,8 @@ | ||
| 1 | 1 | (function(F){ |
| 2 | 2 | /** |
| 3 | - fossil.store is a basic wrapper around localStorage | |
| 3 | + fossil.storage is a basic wrapper around localStorage | |
| 4 | 4 | or sessionStorage or a dummy proxy object if neither |
| 5 | 5 | of those are available. |
| 6 | 6 | */ |
| 7 | 7 | const tryStorage = function f(obj){ |
| 8 | 8 | if(!f.key) f.key = 'fossil.access.check'; |
| 9 | 9 |
| --- src/fossil.storage.js | |
| +++ src/fossil.storage.js | |
| @@ -1,8 +1,8 @@ | |
| 1 | (function(F){ |
| 2 | /** |
| 3 | fossil.store is a basic wrapper around localStorage |
| 4 | or sessionStorage or a dummy proxy object if neither |
| 5 | of those are available. |
| 6 | */ |
| 7 | const tryStorage = function f(obj){ |
| 8 | if(!f.key) f.key = 'fossil.access.check'; |
| 9 |
| --- src/fossil.storage.js | |
| +++ src/fossil.storage.js | |
| @@ -1,8 +1,8 @@ | |
| 1 | (function(F){ |
| 2 | /** |
| 3 | fossil.storage is a basic wrapper around localStorage |
| 4 | or sessionStorage or a dummy proxy object if neither |
| 5 | of those are available. |
| 6 | */ |
| 7 | const tryStorage = function f(obj){ |
| 8 | if(!f.key) f.key = 'fossil.access.check'; |
| 9 |
+9
-3
| --- src/info.c | ||
| +++ src/info.c | ||
| @@ -2248,11 +2248,11 @@ | ||
| 2248 | 2248 | iStart = iEnd = atoi(&zLn[i++]); |
| 2249 | 2249 | }while( zLn[i] && iStart && iEnd ); |
| 2250 | 2250 | } |
| 2251 | 2251 | /*cgi_printf("<!-- ln span count=%d -->", nSpans);*/ |
| 2252 | 2252 | cgi_append_content("<table class='numbered-lines'><tbody>" |
| 2253 | - "<tr><td class='line-numbers'>", -1); | |
| 2253 | + "<tr><td class='line-numbers'><pre>", -1); | |
| 2254 | 2254 | iStart = iEnd = 0; |
| 2255 | 2255 | count_lines(z, nZ, &nLine); |
| 2256 | 2256 | for( n=1 ; n<=nLine; ++n ){ |
| 2257 | 2257 | const char * zAttr = ""; |
| 2258 | 2258 | const char * zId = ""; |
| @@ -2290,13 +2290,14 @@ | ||
| 2290 | 2290 | zAttr = " class='selected-line end'"; |
| 2291 | 2291 | iEnd = 0; |
| 2292 | 2292 | }else if( n>iStart && n<iEnd ){ |
| 2293 | 2293 | zAttr = " class='selected-line'"; |
| 2294 | 2294 | } |
| 2295 | - cgi_printf("<span%s%s>%6d</span>", zId, zAttr, n); | |
| 2295 | + cgi_printf("<span%s%s>%6d</span>\n", zId, zAttr, n) | |
| 2296 | + /* ^^^ explicit \n is necessary for text-mode browsers. */; | |
| 2296 | 2297 | } |
| 2297 | - cgi_append_content("</td><td class='file-content'><pre>",-1); | |
| 2298 | + cgi_append_content("</pre></td><td class='file-content'><pre>",-1); | |
| 2298 | 2299 | if(zExt && *zExt){ |
| 2299 | 2300 | cgi_printf("<code class='language-%h'>",zExt); |
| 2300 | 2301 | }else{ |
| 2301 | 2302 | cgi_append_content("<code>", -1); |
| 2302 | 2303 | } |
| @@ -3304,10 +3305,15 @@ | ||
| 3304 | 3305 | @ <input type="checkbox" name="pclr" checked="checked" /> |
| 3305 | 3306 | }else{ |
| 3306 | 3307 | @ <input type="checkbox" name="pclr" /> |
| 3307 | 3308 | } |
| 3308 | 3309 | @ Propagate color to descendants</label></div> |
| 3310 | + @ <div class='font-size-80'>Be aware that fixed background | |
| 3311 | + @ colors will not interact well with all available skins. | |
| 3312 | + @ It is recommended that fossil be allowed to select these | |
| 3313 | + @ colors automatically so that it can take the skin's | |
| 3314 | + @ preferences into account.</div> | |
| 3309 | 3315 | @ </td></tr> |
| 3310 | 3316 | |
| 3311 | 3317 | @ <tr><th align="right" valign="top">Tags:</th> |
| 3312 | 3318 | @ <td valign="top"> |
| 3313 | 3319 | @ <label><input type="checkbox" id="newtag" name="newtag"%s(zNewTagFlag) /> |
| 3314 | 3320 |
| --- src/info.c | |
| +++ src/info.c | |
| @@ -2248,11 +2248,11 @@ | |
| 2248 | iStart = iEnd = atoi(&zLn[i++]); |
| 2249 | }while( zLn[i] && iStart && iEnd ); |
| 2250 | } |
| 2251 | /*cgi_printf("<!-- ln span count=%d -->", nSpans);*/ |
| 2252 | cgi_append_content("<table class='numbered-lines'><tbody>" |
| 2253 | "<tr><td class='line-numbers'>", -1); |
| 2254 | iStart = iEnd = 0; |
| 2255 | count_lines(z, nZ, &nLine); |
| 2256 | for( n=1 ; n<=nLine; ++n ){ |
| 2257 | const char * zAttr = ""; |
| 2258 | const char * zId = ""; |
| @@ -2290,13 +2290,14 @@ | |
| 2290 | zAttr = " class='selected-line end'"; |
| 2291 | iEnd = 0; |
| 2292 | }else if( n>iStart && n<iEnd ){ |
| 2293 | zAttr = " class='selected-line'"; |
| 2294 | } |
| 2295 | cgi_printf("<span%s%s>%6d</span>", zId, zAttr, n); |
| 2296 | } |
| 2297 | cgi_append_content("</td><td class='file-content'><pre>",-1); |
| 2298 | if(zExt && *zExt){ |
| 2299 | cgi_printf("<code class='language-%h'>",zExt); |
| 2300 | }else{ |
| 2301 | cgi_append_content("<code>", -1); |
| 2302 | } |
| @@ -3304,10 +3305,15 @@ | |
| 3304 | @ <input type="checkbox" name="pclr" checked="checked" /> |
| 3305 | }else{ |
| 3306 | @ <input type="checkbox" name="pclr" /> |
| 3307 | } |
| 3308 | @ Propagate color to descendants</label></div> |
| 3309 | @ </td></tr> |
| 3310 | |
| 3311 | @ <tr><th align="right" valign="top">Tags:</th> |
| 3312 | @ <td valign="top"> |
| 3313 | @ <label><input type="checkbox" id="newtag" name="newtag"%s(zNewTagFlag) /> |
| 3314 |
| --- src/info.c | |
| +++ src/info.c | |
| @@ -2248,11 +2248,11 @@ | |
| 2248 | iStart = iEnd = atoi(&zLn[i++]); |
| 2249 | }while( zLn[i] && iStart && iEnd ); |
| 2250 | } |
| 2251 | /*cgi_printf("<!-- ln span count=%d -->", nSpans);*/ |
| 2252 | cgi_append_content("<table class='numbered-lines'><tbody>" |
| 2253 | "<tr><td class='line-numbers'><pre>", -1); |
| 2254 | iStart = iEnd = 0; |
| 2255 | count_lines(z, nZ, &nLine); |
| 2256 | for( n=1 ; n<=nLine; ++n ){ |
| 2257 | const char * zAttr = ""; |
| 2258 | const char * zId = ""; |
| @@ -2290,13 +2290,14 @@ | |
| 2290 | zAttr = " class='selected-line end'"; |
| 2291 | iEnd = 0; |
| 2292 | }else if( n>iStart && n<iEnd ){ |
| 2293 | zAttr = " class='selected-line'"; |
| 2294 | } |
| 2295 | cgi_printf("<span%s%s>%6d</span>\n", zId, zAttr, n) |
| 2296 | /* ^^^ explicit \n is necessary for text-mode browsers. */; |
| 2297 | } |
| 2298 | cgi_append_content("</pre></td><td class='file-content'><pre>",-1); |
| 2299 | if(zExt && *zExt){ |
| 2300 | cgi_printf("<code class='language-%h'>",zExt); |
| 2301 | }else{ |
| 2302 | cgi_append_content("<code>", -1); |
| 2303 | } |
| @@ -3304,10 +3305,15 @@ | |
| 3305 | @ <input type="checkbox" name="pclr" checked="checked" /> |
| 3306 | }else{ |
| 3307 | @ <input type="checkbox" name="pclr" /> |
| 3308 | } |
| 3309 | @ Propagate color to descendants</label></div> |
| 3310 | @ <div class='font-size-80'>Be aware that fixed background |
| 3311 | @ colors will not interact well with all available skins. |
| 3312 | @ It is recommended that fossil be allowed to select these |
| 3313 | @ colors automatically so that it can take the skin's |
| 3314 | @ preferences into account.</div> |
| 3315 | @ </td></tr> |
| 3316 | |
| 3317 | @ <tr><th align="right" valign="top">Tags:</th> |
| 3318 | @ <td valign="top"> |
| 3319 | @ <label><input type="checkbox" id="newtag" name="newtag"%s(zNewTagFlag) /> |
| 3320 |
+1
| --- src/markdown.c | ||
| +++ src/markdown.c | ||
| @@ -933,10 +933,11 @@ | ||
| 933 | 933 | if(offset>0 && !fossil_isspace(data[-1])){ |
| 934 | 934 | /* Only ever match if the *previous* character is |
| 935 | 935 | whitespace or we're at the start of the input. */ |
| 936 | 936 | return 0; |
| 937 | 937 | } |
| 938 | + assert( '#' == data[0] ); | |
| 938 | 939 | if(size < 2 || !fossil_isalnum(data[1])) return 0; |
| 939 | 940 | /*fprintf(stderr,"HASHREF: %.*s\n", (int)size, data);*/ |
| 940 | 941 | for (end = 2; (end < size) && fossil_isalnum(data[end]); ++end); |
| 941 | 942 | /*TODO: in order to support detection of forum post-style |
| 942 | 943 | references, we need to recognize #X.Y, but only when X and Y are |
| 943 | 944 |
| --- src/markdown.c | |
| +++ src/markdown.c | |
| @@ -933,10 +933,11 @@ | |
| 933 | if(offset>0 && !fossil_isspace(data[-1])){ |
| 934 | /* Only ever match if the *previous* character is |
| 935 | whitespace or we're at the start of the input. */ |
| 936 | return 0; |
| 937 | } |
| 938 | if(size < 2 || !fossil_isalnum(data[1])) return 0; |
| 939 | /*fprintf(stderr,"HASHREF: %.*s\n", (int)size, data);*/ |
| 940 | for (end = 2; (end < size) && fossil_isalnum(data[end]); ++end); |
| 941 | /*TODO: in order to support detection of forum post-style |
| 942 | references, we need to recognize #X.Y, but only when X and Y are |
| 943 |
| --- src/markdown.c | |
| +++ src/markdown.c | |
| @@ -933,10 +933,11 @@ | |
| 933 | if(offset>0 && !fossil_isspace(data[-1])){ |
| 934 | /* Only ever match if the *previous* character is |
| 935 | whitespace or we're at the start of the input. */ |
| 936 | return 0; |
| 937 | } |
| 938 | assert( '#' == data[0] ); |
| 939 | if(size < 2 || !fossil_isalnum(data[1])) return 0; |
| 940 | /*fprintf(stderr,"HASHREF: %.*s\n", (int)size, data);*/ |
| 941 | for (end = 2; (end < size) && fossil_isalnum(data[end]); ++end); |
| 942 | /*TODO: in order to support detection of forum post-style |
| 943 | references, we need to recognize #X.Y, but only when X and Y are |
| 944 |
+1
| --- src/markdown.c | ||
| +++ src/markdown.c | ||
| @@ -933,10 +933,11 @@ | ||
| 933 | 933 | if(offset>0 && !fossil_isspace(data[-1])){ |
| 934 | 934 | /* Only ever match if the *previous* character is |
| 935 | 935 | whitespace or we're at the start of the input. */ |
| 936 | 936 | return 0; |
| 937 | 937 | } |
| 938 | + assert( '#' == data[0] ); | |
| 938 | 939 | if(size < 2 || !fossil_isalnum(data[1])) return 0; |
| 939 | 940 | /*fprintf(stderr,"HASHREF: %.*s\n", (int)size, data);*/ |
| 940 | 941 | for (end = 2; (end < size) && fossil_isalnum(data[end]); ++end); |
| 941 | 942 | /*TODO: in order to support detection of forum post-style |
| 942 | 943 | references, we need to recognize #X.Y, but only when X and Y are |
| 943 | 944 |
| --- src/markdown.c | |
| +++ src/markdown.c | |
| @@ -933,10 +933,11 @@ | |
| 933 | if(offset>0 && !fossil_isspace(data[-1])){ |
| 934 | /* Only ever match if the *previous* character is |
| 935 | whitespace or we're at the start of the input. */ |
| 936 | return 0; |
| 937 | } |
| 938 | if(size < 2 || !fossil_isalnum(data[1])) return 0; |
| 939 | /*fprintf(stderr,"HASHREF: %.*s\n", (int)size, data);*/ |
| 940 | for (end = 2; (end < size) && fossil_isalnum(data[end]); ++end); |
| 941 | /*TODO: in order to support detection of forum post-style |
| 942 | references, we need to recognize #X.Y, but only when X and Y are |
| 943 |
| --- src/markdown.c | |
| +++ src/markdown.c | |
| @@ -933,10 +933,11 @@ | |
| 933 | if(offset>0 && !fossil_isspace(data[-1])){ |
| 934 | /* Only ever match if the *previous* character is |
| 935 | whitespace or we're at the start of the input. */ |
| 936 | return 0; |
| 937 | } |
| 938 | assert( '#' == data[0] ); |
| 939 | if(size < 2 || !fossil_isalnum(data[1])) return 0; |
| 940 | /*fprintf(stderr,"HASHREF: %.*s\n", (int)size, data);*/ |
| 941 | for (end = 2; (end < size) && fossil_isalnum(data[end]); ++end); |
| 942 | /*TODO: in order to support detection of forum post-style |
| 943 | references, we need to recognize #X.Y, but only when X and Y are |
| 944 |
+233
-77
| --- src/shell.c | ||
| +++ src/shell.c | ||
| @@ -651,23 +651,42 @@ | ||
| 651 | 651 | } |
| 652 | 652 | return n; |
| 653 | 653 | } |
| 654 | 654 | |
| 655 | 655 | /* |
| 656 | -** Return true if zFile does not exist or if it is not an ordinary file. | |
| 656 | +** Return open FILE * if zFile exists, can be opened for read | |
| 657 | +** and is an ordinary file or a character stream source. | |
| 658 | +** Otherwise return 0. | |
| 657 | 659 | */ |
| 660 | +static FILE * openChrSource(const char *zFile){ | |
| 658 | 661 | #ifdef _WIN32 |
| 659 | -# define notNormalFile(X) 0 | |
| 662 | + struct _stat x = {0}; | |
| 663 | +# define STAT_CHR_SRC(mode) ((mode & (_S_IFCHR|_S_IFIFO|_S_IFREG))!=0) | |
| 664 | + /* On Windows, open first, then check the stream nature. This order | |
| 665 | + ** is necessary because _stat() and sibs, when checking a named pipe, | |
| 666 | + ** effectively break the pipe as its supplier sees it. */ | |
| 667 | + FILE *rv = fopen(zFile, "rb"); | |
| 668 | + if( rv==0 ) return 0; | |
| 669 | + if( _fstat(_fileno(rv), &x) != 0 | |
| 670 | + || !STAT_CHR_SRC(x.st_mode)){ | |
| 671 | + fclose(rv); | |
| 672 | + rv = 0; | |
| 673 | + } | |
| 674 | + return rv; | |
| 660 | 675 | #else |
| 661 | -static int notNormalFile(const char *zFile){ | |
| 662 | - struct stat x; | |
| 663 | - int rc; | |
| 664 | - memset(&x, 0, sizeof(x)); | |
| 665 | - rc = stat(zFile, &x); | |
| 666 | - return rc || !S_ISREG(x.st_mode); | |
| 667 | -} | |
| 668 | -#endif | |
| 676 | + struct stat x = {0}; | |
| 677 | + int rc = stat(zFile, &x); | |
| 678 | +# define STAT_CHR_SRC(mode) (S_ISREG(mode)||S_ISFIFO(mode)||S_ISCHR(mode)) | |
| 679 | + if( rc!=0 ) return 0; | |
| 680 | + if( STAT_CHR_SRC(x.st_mode) ){ | |
| 681 | + return fopen(zFile, "rb"); | |
| 682 | + }else{ | |
| 683 | + return 0; | |
| 684 | + } | |
| 685 | +#endif | |
| 686 | +#undef STAT_CHR_SRC | |
| 687 | +} | |
| 669 | 688 | |
| 670 | 689 | /* |
| 671 | 690 | ** This routine reads a line of text from FILE in, stores |
| 672 | 691 | ** the text in memory obtained from malloc() and returns a pointer |
| 673 | 692 | ** to the text. NULL is returned at end of file, or if malloc() |
| @@ -2202,10 +2221,15 @@ | ||
| 2202 | 2221 | ** |
| 2203 | 2222 | ** If a non-NULL value is specified for the optional $dir parameter and |
| 2204 | 2223 | ** $path is a relative path, then $path is interpreted relative to $dir. |
| 2205 | 2224 | ** And the paths returned in the "name" column of the table are also |
| 2206 | 2225 | ** relative to directory $dir. |
| 2226 | +** | |
| 2227 | +** Notes on building this extension for Windows: | |
| 2228 | +** Unless linked statically with the SQLite library, a preprocessor | |
| 2229 | +** symbol, FILEIO_WIN32_DLL, must be #define'd to create a stand-alone | |
| 2230 | +** DLL form of this extension for WIN32. See its use below for details. | |
| 2207 | 2231 | */ |
| 2208 | 2232 | /* #include "sqlite3ext.h" */ |
| 2209 | 2233 | SQLITE_EXTENSION_INIT1 |
| 2210 | 2234 | #include <stdio.h> |
| 2211 | 2235 | #include <string.h> |
| @@ -2355,10 +2379,26 @@ | ||
| 2355 | 2379 | fileIntervals.HighPart = pFileTime->dwHighDateTime; |
| 2356 | 2380 | |
| 2357 | 2381 | return (fileIntervals.QuadPart - epochIntervals.QuadPart) / 10000000; |
| 2358 | 2382 | } |
| 2359 | 2383 | |
| 2384 | + | |
| 2385 | +#if defined(FILEIO_WIN32_DLL) && (defined(_WIN32) || defined(WIN32)) | |
| 2386 | +# /* To allow a standalone DLL, use this next replacement function: */ | |
| 2387 | +# undef sqlite3_win32_utf8_to_unicode | |
| 2388 | +# define sqlite3_win32_utf8_to_unicode utf8_to_utf16 | |
| 2389 | +# | |
| 2390 | +LPWSTR utf8_to_utf16(const char *z){ | |
| 2391 | + int nAllot = MultiByteToWideChar(CP_UTF8, 0, z, -1, NULL, 0); | |
| 2392 | + LPWSTR rv = sqlite3_malloc(nAllot * sizeof(WCHAR)); | |
| 2393 | + if( rv!=0 && 0 < MultiByteToWideChar(CP_UTF8, 0, z, -1, rv, nAllot) ) | |
| 2394 | + return rv; | |
| 2395 | + sqlite3_free(rv); | |
| 2396 | + return 0; | |
| 2397 | +} | |
| 2398 | +#endif | |
| 2399 | + | |
| 2360 | 2400 | /* |
| 2361 | 2401 | ** This function attempts to normalize the time values found in the stat() |
| 2362 | 2402 | ** buffer to UTC. This is necessary on Win32, where the runtime library |
| 2363 | 2403 | ** appears to return these values as local times. |
| 2364 | 2404 | */ |
| @@ -3128,10 +3168,18 @@ | ||
| 3128 | 3168 | if( rc==SQLITE_OK ){ |
| 3129 | 3169 | rc = fsdirRegister(db); |
| 3130 | 3170 | } |
| 3131 | 3171 | return rc; |
| 3132 | 3172 | } |
| 3173 | + | |
| 3174 | +#if defined(FILEIO_WIN32_DLL) && (defined(_WIN32) || defined(WIN32)) | |
| 3175 | +/* To allow a standalone DLL, make test_windirent.c use the same | |
| 3176 | + * redefined SQLite API calls as the above extension code does. | |
| 3177 | + * Just pull in this .c to accomplish this. As a beneficial side | |
| 3178 | + * effect, this extension becomes a single translation unit. */ | |
| 3179 | +# include "test_windirent.c" | |
| 3180 | +#endif | |
| 3133 | 3181 | |
| 3134 | 3182 | /************************* End ../ext/misc/fileio.c ********************/ |
| 3135 | 3183 | /************************* Begin ../ext/misc/completion.c ******************/ |
| 3136 | 3184 | /* |
| 3137 | 3185 | ** 2017-07-10 |
| @@ -10099,10 +10147,23 @@ | ||
| 10099 | 10147 | idxFinalize(&rc, pIdxList); |
| 10100 | 10148 | |
| 10101 | 10149 | *pRc = rc; |
| 10102 | 10150 | return 0; |
| 10103 | 10151 | } |
| 10152 | + | |
| 10153 | +/* Callback for sqlite3_exec() with query with leading count(*) column. | |
| 10154 | + * The first argument is expected to be an int*, referent to be incremented | |
| 10155 | + * if that leading column is not exactly '0'. | |
| 10156 | + */ | |
| 10157 | +static int countNonzeros(void* pCount, int nc, | |
| 10158 | + char* azResults[], char* azColumns[]){ | |
| 10159 | + (void)azColumns; /* Suppress unused parameter warning */ | |
| 10160 | + if( nc>0 && (azResults[0][0]!='0' || azResults[0][1]!=0) ){ | |
| 10161 | + *((int *)pCount) += 1; | |
| 10162 | + } | |
| 10163 | + return 0; | |
| 10164 | +} | |
| 10104 | 10165 | |
| 10105 | 10166 | static int idxCreateFromCons( |
| 10106 | 10167 | sqlite3expert *p, |
| 10107 | 10168 | IdxScan *pScan, |
| 10108 | 10169 | IdxConstraint *pEq, |
| @@ -10126,30 +10187,57 @@ | ||
| 10126 | 10187 | } |
| 10127 | 10188 | |
| 10128 | 10189 | if( rc==SQLITE_OK ){ |
| 10129 | 10190 | /* Hash the list of columns to come up with a name for the index */ |
| 10130 | 10191 | const char *zTable = pScan->pTab->zName; |
| 10131 | - char *zName; /* Index name */ | |
| 10132 | - int i; | |
| 10133 | - for(i=0; zCols[i]; i++){ | |
| 10134 | - h += ((h<<3) + zCols[i]); | |
| 10135 | - } | |
| 10136 | - zName = sqlite3_mprintf("%s_idx_%08x", zTable, h); | |
| 10137 | - if( zName==0 ){ | |
| 10192 | + int quoteTable = idxIdentifierRequiresQuotes(zTable); | |
| 10193 | + char *zName = 0; /* Index name */ | |
| 10194 | + int collisions = 0; | |
| 10195 | + do{ | |
| 10196 | + int i; | |
| 10197 | + char *zFind; | |
| 10198 | + for(i=0; zCols[i]; i++){ | |
| 10199 | + h += ((h<<3) + zCols[i]); | |
| 10200 | + } | |
| 10201 | + sqlite3_free(zName); | |
| 10202 | + zName = sqlite3_mprintf("%s_idx_%08x", zTable, h); | |
| 10203 | + if( zName==0 ) break; | |
| 10204 | + /* Is is unique among table, view and index names? */ | |
| 10205 | + zFmt = "SELECT count(*) FROM sqlite_schema WHERE name=%Q" | |
| 10206 | + " AND type in ('index','table','view')"; | |
| 10207 | + zFind = sqlite3_mprintf(zFmt, zName); | |
| 10208 | + i = 0; | |
| 10209 | + rc = sqlite3_exec(dbm, zFind, countNonzeros, &i, 0); | |
| 10210 | + assert(rc==SQLITE_OK); | |
| 10211 | + sqlite3_free(zFind); | |
| 10212 | + if( i==0 ){ | |
| 10213 | + collisions = 0; | |
| 10214 | + break; | |
| 10215 | + } | |
| 10216 | + ++collisions; | |
| 10217 | + }while( collisions<50 && zName!=0 ); | |
| 10218 | + if( collisions ){ | |
| 10219 | + /* This return means "Gave up trying to find a unique index name." */ | |
| 10220 | + rc = SQLITE_BUSY_TIMEOUT; | |
| 10221 | + }else if( zName==0 ){ | |
| 10138 | 10222 | rc = SQLITE_NOMEM; |
| 10139 | 10223 | }else{ |
| 10140 | - if( idxIdentifierRequiresQuotes(zTable) ){ | |
| 10141 | - zFmt = "CREATE INDEX '%q' ON %Q(%s)"; | |
| 10224 | + if( quoteTable ){ | |
| 10225 | + zFmt = "CREATE INDEX \"%w\" ON \"%w\"(%s)"; | |
| 10142 | 10226 | }else{ |
| 10143 | 10227 | zFmt = "CREATE INDEX %s ON %s(%s)"; |
| 10144 | 10228 | } |
| 10145 | 10229 | zIdx = sqlite3_mprintf(zFmt, zName, zTable, zCols); |
| 10146 | 10230 | if( !zIdx ){ |
| 10147 | 10231 | rc = SQLITE_NOMEM; |
| 10148 | 10232 | }else{ |
| 10149 | 10233 | rc = sqlite3_exec(dbm, zIdx, 0, 0, p->pzErrmsg); |
| 10150 | - idxHashAdd(&rc, &p->hIdx, zName, zIdx); | |
| 10234 | + if( rc!=SQLITE_OK ){ | |
| 10235 | + rc = SQLITE_BUSY_TIMEOUT; | |
| 10236 | + }else{ | |
| 10237 | + idxHashAdd(&rc, &p->hIdx, zName, zIdx); | |
| 10238 | + } | |
| 10151 | 10239 | } |
| 10152 | 10240 | sqlite3_free(zName); |
| 10153 | 10241 | sqlite3_free(zIdx); |
| 10154 | 10242 | } |
| 10155 | 10243 | } |
| @@ -11069,10 +11157,14 @@ | ||
| 11069 | 11157 | rc = idxProcessTriggers(p, pzErr); |
| 11070 | 11158 | |
| 11071 | 11159 | /* Create candidate indexes within the in-memory database file */ |
| 11072 | 11160 | if( rc==SQLITE_OK ){ |
| 11073 | 11161 | rc = idxCreateCandidates(p); |
| 11162 | + }else if ( rc==SQLITE_BUSY_TIMEOUT ){ | |
| 11163 | + if( pzErr ) | |
| 11164 | + *pzErr = sqlite3_mprintf("Cannot find a unique index name to propose."); | |
| 11165 | + return rc; | |
| 11074 | 11166 | } |
| 11075 | 11167 | |
| 11076 | 11168 | /* Generate the stat1 data */ |
| 11077 | 11169 | if( rc==SQLITE_OK ){ |
| 11078 | 11170 | rc = idxPopulateStat1(p, pzErr); |
| @@ -12161,11 +12253,11 @@ | ||
| 12161 | 12253 | #define SHFLG_Backslash 0x00000004 /* The --backslash option is used */ |
| 12162 | 12254 | #define SHFLG_PreserveRowid 0x00000008 /* .dump preserves rowid values */ |
| 12163 | 12255 | #define SHFLG_Newlines 0x00000010 /* .dump --newline flag */ |
| 12164 | 12256 | #define SHFLG_CountChanges 0x00000020 /* .changes setting */ |
| 12165 | 12257 | #define SHFLG_Echo 0x00000040 /* .echo or --echo setting */ |
| 12166 | -#define SHFLG_HeaderSet 0x00000080 /* .header has been used */ | |
| 12258 | +#define SHFLG_HeaderSet 0x00000080 /* showHeader has been specified */ | |
| 12167 | 12259 | #define SHFLG_DumpDataOnly 0x00000100 /* .dump show data only */ |
| 12168 | 12260 | #define SHFLG_DumpNoSys 0x00000200 /* .dump omits system tables */ |
| 12169 | 12261 | |
| 12170 | 12262 | /* |
| 12171 | 12263 | ** Macros for testing and setting shellFlgs |
| @@ -20197,11 +20289,11 @@ | ||
| 20197 | 20289 | }else{ |
| 20198 | 20290 | rc = process_input(p); |
| 20199 | 20291 | pclose(p->in); |
| 20200 | 20292 | } |
| 20201 | 20293 | #endif |
| 20202 | - }else if( notNormalFile(azArg[1]) || (p->in = fopen(azArg[1], "rb"))==0 ){ | |
| 20294 | + }else if( (p->in = openChrSource(azArg[1]))==0 ){ | |
| 20203 | 20295 | utf8_printf(stderr,"Error: cannot open \"%s\"\n", azArg[1]); |
| 20204 | 20296 | rc = 1; |
| 20205 | 20297 | }else{ |
| 20206 | 20298 | rc = process_input(p); |
| 20207 | 20299 | fclose(p->in); |
| @@ -21556,59 +21648,114 @@ | ||
| 21556 | 21648 | } |
| 21557 | 21649 | p->bSafeMode = p->bSafeModePersist; |
| 21558 | 21650 | return rc; |
| 21559 | 21651 | } |
| 21560 | 21652 | |
| 21561 | -/* | |
| 21562 | -** Return TRUE if a semicolon occurs anywhere in the first N characters | |
| 21563 | -** of string z[]. | |
| 21564 | -*/ | |
| 21565 | -static int line_contains_semicolon(const char *z, int N){ | |
| 21566 | - int i; | |
| 21567 | - for(i=0; i<N; i++){ if( z[i]==';' ) return 1; } | |
| 21568 | - return 0; | |
| 21569 | -} | |
| 21570 | - | |
| 21571 | -/* | |
| 21572 | -** Test to see if a line consists entirely of whitespace. | |
| 21573 | -*/ | |
| 21574 | -static int _all_whitespace(const char *z){ | |
| 21575 | - for(; *z; z++){ | |
| 21576 | - if( IsSpace(z[0]) ) continue; | |
| 21577 | - if( *z=='/' && z[1]=='*' ){ | |
| 21578 | - z += 2; | |
| 21579 | - while( *z && (*z!='*' || z[1]!='/') ){ z++; } | |
| 21580 | - if( *z==0 ) return 0; | |
| 21581 | - z++; | |
| 21582 | - continue; | |
| 21583 | - } | |
| 21584 | - if( *z=='-' && z[1]=='-' ){ | |
| 21585 | - z += 2; | |
| 21586 | - while( *z && *z!='\n' ){ z++; } | |
| 21587 | - if( *z==0 ) return 1; | |
| 21588 | - continue; | |
| 21589 | - } | |
| 21590 | - return 0; | |
| 21591 | - } | |
| 21592 | - return 1; | |
| 21653 | +/* Line scan result and intermediate states (supporting scan resumption) | |
| 21654 | +*/ | |
| 21655 | +#ifndef CHAR_BIT | |
| 21656 | +# define CHAR_BIT 8 | |
| 21657 | +#endif | |
| 21658 | +typedef enum { | |
| 21659 | + QSS_HasDark = 1<<CHAR_BIT, QSS_EndingSemi = 2<<CHAR_BIT, | |
| 21660 | + QSS_CharMask = (1<<CHAR_BIT)-1, QSS_ScanMask = 3<<CHAR_BIT, | |
| 21661 | + QSS_Start = 0 | |
| 21662 | +} QuickScanState; | |
| 21663 | +#define QSS_SETV(qss, newst) ((newst) | ((qss) & QSS_ScanMask)) | |
| 21664 | +#define QSS_INPLAIN(qss) (((qss)&QSS_CharMask)==QSS_Start) | |
| 21665 | +#define QSS_PLAINWHITE(qss) (((qss)&~QSS_EndingSemi)==QSS_Start) | |
| 21666 | +#define QSS_PLAINDARK(qss) (((qss)&~QSS_EndingSemi)==QSS_HasDark) | |
| 21667 | +#define QSS_SEMITERM(qss) (((qss)&~QSS_HasDark)==QSS_EndingSemi) | |
| 21668 | + | |
| 21669 | +/* | |
| 21670 | +** Scan line for classification to guide shell's handling. | |
| 21671 | +** The scan is resumable for subsequent lines when prior | |
| 21672 | +** return values are passed as the 2nd argument. | |
| 21673 | +*/ | |
| 21674 | +static QuickScanState quickscan(char *zLine, QuickScanState qss){ | |
| 21675 | + char cin; | |
| 21676 | + char cWait = (char)qss; /* intentional narrowing loss */ | |
| 21677 | + if( cWait==0 ){ | |
| 21678 | + PlainScan: | |
| 21679 | + while( (cin = *zLine++)!=0 ){ | |
| 21680 | + if( IsSpace(cin) ) | |
| 21681 | + continue; | |
| 21682 | + switch (cin){ | |
| 21683 | + case '-': | |
| 21684 | + if( *zLine!='-' ) | |
| 21685 | + break; | |
| 21686 | + while((cin = *++zLine)!=0 ) | |
| 21687 | + if( cin=='\n') | |
| 21688 | + goto PlainScan; | |
| 21689 | + return qss; | |
| 21690 | + case ';': | |
| 21691 | + qss |= QSS_EndingSemi; | |
| 21692 | + continue; | |
| 21693 | + case '/': | |
| 21694 | + if( *zLine=='*' ){ | |
| 21695 | + ++zLine; | |
| 21696 | + cWait = '*'; | |
| 21697 | + qss = QSS_SETV(qss, cWait); | |
| 21698 | + goto TermScan; | |
| 21699 | + } | |
| 21700 | + break; | |
| 21701 | + case '[': | |
| 21702 | + cin = ']'; | |
| 21703 | + /* fall thru */ | |
| 21704 | + case '`': case '\'': case '"': | |
| 21705 | + cWait = cin; | |
| 21706 | + qss = QSS_HasDark | cWait; | |
| 21707 | + goto TermScan; | |
| 21708 | + default: | |
| 21709 | + break; | |
| 21710 | + } | |
| 21711 | + qss = (qss & ~QSS_EndingSemi) | QSS_HasDark; | |
| 21712 | + } | |
| 21713 | + }else{ | |
| 21714 | + TermScan: | |
| 21715 | + while( (cin = *zLine++)!=0 ){ | |
| 21716 | + if( cin==cWait ){ | |
| 21717 | + switch( cWait ){ | |
| 21718 | + case '*': | |
| 21719 | + if( *zLine != '/' ) | |
| 21720 | + continue; | |
| 21721 | + ++zLine; | |
| 21722 | + cWait = 0; | |
| 21723 | + qss = QSS_SETV(qss, 0); | |
| 21724 | + goto PlainScan; | |
| 21725 | + case '`': case '\'': case '"': | |
| 21726 | + if(*zLine==cWait){ | |
| 21727 | + ++zLine; | |
| 21728 | + continue; | |
| 21729 | + } | |
| 21730 | + /* fall thru */ | |
| 21731 | + case ']': | |
| 21732 | + cWait = 0; | |
| 21733 | + qss = QSS_SETV(qss, 0); | |
| 21734 | + goto PlainScan; | |
| 21735 | + default: assert(0); | |
| 21736 | + } | |
| 21737 | + } | |
| 21738 | + } | |
| 21739 | + } | |
| 21740 | + return qss; | |
| 21593 | 21741 | } |
| 21594 | 21742 | |
| 21595 | 21743 | /* |
| 21596 | 21744 | ** Return TRUE if the line typed in is an SQL command terminator other |
| 21597 | 21745 | ** than a semi-colon. The SQL Server style "go" command is understood |
| 21598 | 21746 | ** as is the Oracle "/". |
| 21599 | 21747 | */ |
| 21600 | -static int line_is_command_terminator(const char *zLine){ | |
| 21748 | +static int line_is_command_terminator(char *zLine){ | |
| 21601 | 21749 | while( IsSpace(zLine[0]) ){ zLine++; }; |
| 21602 | - if( zLine[0]=='/' && _all_whitespace(&zLine[1]) ){ | |
| 21603 | - return 1; /* Oracle */ | |
| 21604 | - } | |
| 21605 | - if( ToLower(zLine[0])=='g' && ToLower(zLine[1])=='o' | |
| 21606 | - && _all_whitespace(&zLine[2]) ){ | |
| 21607 | - return 1; /* SQL Server */ | |
| 21608 | - } | |
| 21609 | - return 0; | |
| 21750 | + if( zLine[0]=='/' ) | |
| 21751 | + zLine += 1; /* Oracle */ | |
| 21752 | + else if ( ToLower(zLine[0])=='g' && ToLower(zLine[1])=='o' ) | |
| 21753 | + zLine += 2; /* SQL Server */ | |
| 21754 | + else | |
| 21755 | + return 0; | |
| 21756 | + return quickscan(zLine,QSS_Start)==QSS_Start; | |
| 21610 | 21757 | } |
| 21611 | 21758 | |
| 21612 | 21759 | /* |
| 21613 | 21760 | ** We need a default sqlite3_complete() implementation to use in case |
| 21614 | 21761 | ** the shell is compiled with SQLITE_OMIT_COMPLETE. The default assumes |
| @@ -21661,12 +21808,15 @@ | ||
| 21661 | 21808 | }else{ |
| 21662 | 21809 | utf8_printf(stderr, "%s %s\n", zPrefix, sqlite3_errmsg(p->db)); |
| 21663 | 21810 | } |
| 21664 | 21811 | return 1; |
| 21665 | 21812 | }else if( ShellHasFlag(p, SHFLG_CountChanges) ){ |
| 21666 | - raw_printf(p->out, "changes: %3lld total_changes: %lld\n", | |
| 21813 | + char zLineBuf[2000]; | |
| 21814 | + sqlite3_snprintf(sizeof(zLineBuf), zLineBuf, | |
| 21815 | + "changes: %lld total_changes: %lld", | |
| 21667 | 21816 | sqlite3_changes64(p->db), sqlite3_total_changes64(p->db)); |
| 21817 | + raw_printf(p->out, "%s\n", zLineBuf); | |
| 21668 | 21818 | } |
| 21669 | 21819 | return 0; |
| 21670 | 21820 | } |
| 21671 | 21821 | |
| 21672 | 21822 | |
| @@ -21683,14 +21833,14 @@ | ||
| 21683 | 21833 | char *zLine = 0; /* A single input line */ |
| 21684 | 21834 | char *zSql = 0; /* Accumulated SQL text */ |
| 21685 | 21835 | int nLine; /* Length of current line */ |
| 21686 | 21836 | int nSql = 0; /* Bytes of zSql[] used */ |
| 21687 | 21837 | int nAlloc = 0; /* Allocated zSql[] space */ |
| 21688 | - int nSqlPrior = 0; /* Bytes of zSql[] used by prior line */ | |
| 21689 | 21838 | int rc; /* Error code */ |
| 21690 | 21839 | int errCnt = 0; /* Number of errors seen */ |
| 21691 | 21840 | int startline = 0; /* Line number for start of current input */ |
| 21841 | + QuickScanState qss = QSS_Start; /* Accumulated line status (so far) */ | |
| 21692 | 21842 | |
| 21693 | 21843 | p->lineno = 0; |
| 21694 | 21844 | while( errCnt==0 || !bail_on_error || (p->in==0 && stdin_is_interactive) ){ |
| 21695 | 21845 | fflush(p->out); |
| 21696 | 21846 | zLine = one_input_line(p->in, zLine, nSql>0); |
| @@ -21702,12 +21852,20 @@ | ||
| 21702 | 21852 | if( seenInterrupt ){ |
| 21703 | 21853 | if( p->in!=0 ) break; |
| 21704 | 21854 | seenInterrupt = 0; |
| 21705 | 21855 | } |
| 21706 | 21856 | p->lineno++; |
| 21707 | - if( nSql==0 && _all_whitespace(zLine) ){ | |
| 21708 | - if( ShellHasFlag(p, SHFLG_Echo) ) printf("%s\n", zLine); | |
| 21857 | + if( QSS_INPLAIN(qss) | |
| 21858 | + && line_is_command_terminator(zLine) | |
| 21859 | + && line_is_complete(zSql, nSql) ){ | |
| 21860 | + memcpy(zLine,";",2); | |
| 21861 | + } | |
| 21862 | + qss = quickscan(zLine, qss); | |
| 21863 | + if( QSS_PLAINWHITE(qss) && nSql==0 ){ | |
| 21864 | + if( ShellHasFlag(p, SHFLG_Echo) ) | |
| 21865 | + printf("%s\n", zLine); | |
| 21866 | + /* Just swallow leading whitespace */ | |
| 21709 | 21867 | continue; |
| 21710 | 21868 | } |
| 21711 | 21869 | if( zLine && (zLine[0]=='.' || zLine[0]=='#') && nSql==0 ){ |
| 21712 | 21870 | if( ShellHasFlag(p, SHFLG_Echo) ) printf("%s\n", zLine); |
| 21713 | 21871 | if( zLine[0]=='.' ){ |
| @@ -21718,20 +21876,17 @@ | ||
| 21718 | 21876 | errCnt++; |
| 21719 | 21877 | } |
| 21720 | 21878 | } |
| 21721 | 21879 | continue; |
| 21722 | 21880 | } |
| 21723 | - if( line_is_command_terminator(zLine) && line_is_complete(zSql, nSql) ){ | |
| 21724 | - memcpy(zLine,";",2); | |
| 21725 | - } | |
| 21726 | 21881 | nLine = strlen30(zLine); |
| 21727 | 21882 | if( nSql+nLine+2>=nAlloc ){ |
| 21728 | - nAlloc = nSql+nLine+100; | |
| 21883 | + /* Grow buffer by half-again increments when big. */ | |
| 21884 | + nAlloc = nSql+(nSql>>1)+nLine+100; | |
| 21729 | 21885 | zSql = realloc(zSql, nAlloc); |
| 21730 | 21886 | if( zSql==0 ) shell_out_of_memory(); |
| 21731 | 21887 | } |
| 21732 | - nSqlPrior = nSql; | |
| 21733 | 21888 | if( nSql==0 ){ |
| 21734 | 21889 | int i; |
| 21735 | 21890 | for(i=0; zLine[i] && IsSpace(zLine[i]); i++){} |
| 21736 | 21891 | assert( nAlloc>0 && zSql!=0 ); |
| 21737 | 21892 | memcpy(zSql, zLine+i, nLine+1-i); |
| @@ -21740,27 +21895,26 @@ | ||
| 21740 | 21895 | }else{ |
| 21741 | 21896 | zSql[nSql++] = '\n'; |
| 21742 | 21897 | memcpy(zSql+nSql, zLine, nLine+1); |
| 21743 | 21898 | nSql += nLine; |
| 21744 | 21899 | } |
| 21745 | - if( nSql && line_contains_semicolon(&zSql[nSqlPrior], nSql-nSqlPrior) | |
| 21746 | - && sqlite3_complete(zSql) ){ | |
| 21900 | + if( nSql && QSS_SEMITERM(qss) && sqlite3_complete(zSql) ){ | |
| 21747 | 21901 | errCnt += runOneSqlLine(p, zSql, p->in, startline); |
| 21748 | 21902 | nSql = 0; |
| 21749 | 21903 | if( p->outCount ){ |
| 21750 | 21904 | output_reset(p); |
| 21751 | 21905 | p->outCount = 0; |
| 21752 | 21906 | }else{ |
| 21753 | 21907 | clearTempFile(p); |
| 21754 | 21908 | } |
| 21755 | 21909 | p->bSafeMode = p->bSafeModePersist; |
| 21756 | - }else if( nSql && _all_whitespace(zSql) ){ | |
| 21910 | + }else if( nSql && QSS_PLAINWHITE(qss) ){ | |
| 21757 | 21911 | if( ShellHasFlag(p, SHFLG_Echo) ) printf("%s\n", zSql); |
| 21758 | 21912 | nSql = 0; |
| 21759 | 21913 | } |
| 21760 | 21914 | } |
| 21761 | - if( nSql && !_all_whitespace(zSql) ){ | |
| 21915 | + if( nSql && QSS_PLAINDARK(qss) ){ | |
| 21762 | 21916 | errCnt += runOneSqlLine(p, zSql, p->in, startline); |
| 21763 | 21917 | } |
| 21764 | 21918 | free(zSql); |
| 21765 | 21919 | free(zLine); |
| 21766 | 21920 | return errCnt>0; |
| @@ -22399,12 +22553,14 @@ | ||
| 22399 | 22553 | }else if( strcmp(z,"-nullvalue")==0 ){ |
| 22400 | 22554 | sqlite3_snprintf(sizeof(data.nullValue), data.nullValue, |
| 22401 | 22555 | "%s",cmdline_option_value(argc,argv,++i)); |
| 22402 | 22556 | }else if( strcmp(z,"-header")==0 ){ |
| 22403 | 22557 | data.showHeader = 1; |
| 22404 | - }else if( strcmp(z,"-noheader")==0 ){ | |
| 22558 | + ShellSetFlag(&data, SHFLG_HeaderSet); | |
| 22559 | + }else if( strcmp(z,"-noheader")==0 ){ | |
| 22405 | 22560 | data.showHeader = 0; |
| 22561 | + ShellSetFlag(&data, SHFLG_HeaderSet); | |
| 22406 | 22562 | }else if( strcmp(z,"-echo")==0 ){ |
| 22407 | 22563 | ShellSetFlag(&data, SHFLG_Echo); |
| 22408 | 22564 | }else if( strcmp(z,"-eqp")==0 ){ |
| 22409 | 22565 | data.autoEQP = AUTOEQP_on; |
| 22410 | 22566 | }else if( strcmp(z,"-eqpfull")==0 ){ |
| 22411 | 22567 |
| --- src/shell.c | |
| +++ src/shell.c | |
| @@ -651,23 +651,42 @@ | |
| 651 | } |
| 652 | return n; |
| 653 | } |
| 654 | |
| 655 | /* |
| 656 | ** Return true if zFile does not exist or if it is not an ordinary file. |
| 657 | */ |
| 658 | #ifdef _WIN32 |
| 659 | # define notNormalFile(X) 0 |
| 660 | #else |
| 661 | static int notNormalFile(const char *zFile){ |
| 662 | struct stat x; |
| 663 | int rc; |
| 664 | memset(&x, 0, sizeof(x)); |
| 665 | rc = stat(zFile, &x); |
| 666 | return rc || !S_ISREG(x.st_mode); |
| 667 | } |
| 668 | #endif |
| 669 | |
| 670 | /* |
| 671 | ** This routine reads a line of text from FILE in, stores |
| 672 | ** the text in memory obtained from malloc() and returns a pointer |
| 673 | ** to the text. NULL is returned at end of file, or if malloc() |
| @@ -2202,10 +2221,15 @@ | |
| 2202 | ** |
| 2203 | ** If a non-NULL value is specified for the optional $dir parameter and |
| 2204 | ** $path is a relative path, then $path is interpreted relative to $dir. |
| 2205 | ** And the paths returned in the "name" column of the table are also |
| 2206 | ** relative to directory $dir. |
| 2207 | */ |
| 2208 | /* #include "sqlite3ext.h" */ |
| 2209 | SQLITE_EXTENSION_INIT1 |
| 2210 | #include <stdio.h> |
| 2211 | #include <string.h> |
| @@ -2355,10 +2379,26 @@ | |
| 2355 | fileIntervals.HighPart = pFileTime->dwHighDateTime; |
| 2356 | |
| 2357 | return (fileIntervals.QuadPart - epochIntervals.QuadPart) / 10000000; |
| 2358 | } |
| 2359 | |
| 2360 | /* |
| 2361 | ** This function attempts to normalize the time values found in the stat() |
| 2362 | ** buffer to UTC. This is necessary on Win32, where the runtime library |
| 2363 | ** appears to return these values as local times. |
| 2364 | */ |
| @@ -3128,10 +3168,18 @@ | |
| 3128 | if( rc==SQLITE_OK ){ |
| 3129 | rc = fsdirRegister(db); |
| 3130 | } |
| 3131 | return rc; |
| 3132 | } |
| 3133 | |
| 3134 | /************************* End ../ext/misc/fileio.c ********************/ |
| 3135 | /************************* Begin ../ext/misc/completion.c ******************/ |
| 3136 | /* |
| 3137 | ** 2017-07-10 |
| @@ -10099,10 +10147,23 @@ | |
| 10099 | idxFinalize(&rc, pIdxList); |
| 10100 | |
| 10101 | *pRc = rc; |
| 10102 | return 0; |
| 10103 | } |
| 10104 | |
| 10105 | static int idxCreateFromCons( |
| 10106 | sqlite3expert *p, |
| 10107 | IdxScan *pScan, |
| 10108 | IdxConstraint *pEq, |
| @@ -10126,30 +10187,57 @@ | |
| 10126 | } |
| 10127 | |
| 10128 | if( rc==SQLITE_OK ){ |
| 10129 | /* Hash the list of columns to come up with a name for the index */ |
| 10130 | const char *zTable = pScan->pTab->zName; |
| 10131 | char *zName; /* Index name */ |
| 10132 | int i; |
| 10133 | for(i=0; zCols[i]; i++){ |
| 10134 | h += ((h<<3) + zCols[i]); |
| 10135 | } |
| 10136 | zName = sqlite3_mprintf("%s_idx_%08x", zTable, h); |
| 10137 | if( zName==0 ){ |
| 10138 | rc = SQLITE_NOMEM; |
| 10139 | }else{ |
| 10140 | if( idxIdentifierRequiresQuotes(zTable) ){ |
| 10141 | zFmt = "CREATE INDEX '%q' ON %Q(%s)"; |
| 10142 | }else{ |
| 10143 | zFmt = "CREATE INDEX %s ON %s(%s)"; |
| 10144 | } |
| 10145 | zIdx = sqlite3_mprintf(zFmt, zName, zTable, zCols); |
| 10146 | if( !zIdx ){ |
| 10147 | rc = SQLITE_NOMEM; |
| 10148 | }else{ |
| 10149 | rc = sqlite3_exec(dbm, zIdx, 0, 0, p->pzErrmsg); |
| 10150 | idxHashAdd(&rc, &p->hIdx, zName, zIdx); |
| 10151 | } |
| 10152 | sqlite3_free(zName); |
| 10153 | sqlite3_free(zIdx); |
| 10154 | } |
| 10155 | } |
| @@ -11069,10 +11157,14 @@ | |
| 11069 | rc = idxProcessTriggers(p, pzErr); |
| 11070 | |
| 11071 | /* Create candidate indexes within the in-memory database file */ |
| 11072 | if( rc==SQLITE_OK ){ |
| 11073 | rc = idxCreateCandidates(p); |
| 11074 | } |
| 11075 | |
| 11076 | /* Generate the stat1 data */ |
| 11077 | if( rc==SQLITE_OK ){ |
| 11078 | rc = idxPopulateStat1(p, pzErr); |
| @@ -12161,11 +12253,11 @@ | |
| 12161 | #define SHFLG_Backslash 0x00000004 /* The --backslash option is used */ |
| 12162 | #define SHFLG_PreserveRowid 0x00000008 /* .dump preserves rowid values */ |
| 12163 | #define SHFLG_Newlines 0x00000010 /* .dump --newline flag */ |
| 12164 | #define SHFLG_CountChanges 0x00000020 /* .changes setting */ |
| 12165 | #define SHFLG_Echo 0x00000040 /* .echo or --echo setting */ |
| 12166 | #define SHFLG_HeaderSet 0x00000080 /* .header has been used */ |
| 12167 | #define SHFLG_DumpDataOnly 0x00000100 /* .dump show data only */ |
| 12168 | #define SHFLG_DumpNoSys 0x00000200 /* .dump omits system tables */ |
| 12169 | |
| 12170 | /* |
| 12171 | ** Macros for testing and setting shellFlgs |
| @@ -20197,11 +20289,11 @@ | |
| 20197 | }else{ |
| 20198 | rc = process_input(p); |
| 20199 | pclose(p->in); |
| 20200 | } |
| 20201 | #endif |
| 20202 | }else if( notNormalFile(azArg[1]) || (p->in = fopen(azArg[1], "rb"))==0 ){ |
| 20203 | utf8_printf(stderr,"Error: cannot open \"%s\"\n", azArg[1]); |
| 20204 | rc = 1; |
| 20205 | }else{ |
| 20206 | rc = process_input(p); |
| 20207 | fclose(p->in); |
| @@ -21556,59 +21648,114 @@ | |
| 21556 | } |
| 21557 | p->bSafeMode = p->bSafeModePersist; |
| 21558 | return rc; |
| 21559 | } |
| 21560 | |
| 21561 | /* |
| 21562 | ** Return TRUE if a semicolon occurs anywhere in the first N characters |
| 21563 | ** of string z[]. |
| 21564 | */ |
| 21565 | static int line_contains_semicolon(const char *z, int N){ |
| 21566 | int i; |
| 21567 | for(i=0; i<N; i++){ if( z[i]==';' ) return 1; } |
| 21568 | return 0; |
| 21569 | } |
| 21570 | |
| 21571 | /* |
| 21572 | ** Test to see if a line consists entirely of whitespace. |
| 21573 | */ |
| 21574 | static int _all_whitespace(const char *z){ |
| 21575 | for(; *z; z++){ |
| 21576 | if( IsSpace(z[0]) ) continue; |
| 21577 | if( *z=='/' && z[1]=='*' ){ |
| 21578 | z += 2; |
| 21579 | while( *z && (*z!='*' || z[1]!='/') ){ z++; } |
| 21580 | if( *z==0 ) return 0; |
| 21581 | z++; |
| 21582 | continue; |
| 21583 | } |
| 21584 | if( *z=='-' && z[1]=='-' ){ |
| 21585 | z += 2; |
| 21586 | while( *z && *z!='\n' ){ z++; } |
| 21587 | if( *z==0 ) return 1; |
| 21588 | continue; |
| 21589 | } |
| 21590 | return 0; |
| 21591 | } |
| 21592 | return 1; |
| 21593 | } |
| 21594 | |
| 21595 | /* |
| 21596 | ** Return TRUE if the line typed in is an SQL command terminator other |
| 21597 | ** than a semi-colon. The SQL Server style "go" command is understood |
| 21598 | ** as is the Oracle "/". |
| 21599 | */ |
| 21600 | static int line_is_command_terminator(const char *zLine){ |
| 21601 | while( IsSpace(zLine[0]) ){ zLine++; }; |
| 21602 | if( zLine[0]=='/' && _all_whitespace(&zLine[1]) ){ |
| 21603 | return 1; /* Oracle */ |
| 21604 | } |
| 21605 | if( ToLower(zLine[0])=='g' && ToLower(zLine[1])=='o' |
| 21606 | && _all_whitespace(&zLine[2]) ){ |
| 21607 | return 1; /* SQL Server */ |
| 21608 | } |
| 21609 | return 0; |
| 21610 | } |
| 21611 | |
| 21612 | /* |
| 21613 | ** We need a default sqlite3_complete() implementation to use in case |
| 21614 | ** the shell is compiled with SQLITE_OMIT_COMPLETE. The default assumes |
| @@ -21661,12 +21808,15 @@ | |
| 21661 | }else{ |
| 21662 | utf8_printf(stderr, "%s %s\n", zPrefix, sqlite3_errmsg(p->db)); |
| 21663 | } |
| 21664 | return 1; |
| 21665 | }else if( ShellHasFlag(p, SHFLG_CountChanges) ){ |
| 21666 | raw_printf(p->out, "changes: %3lld total_changes: %lld\n", |
| 21667 | sqlite3_changes64(p->db), sqlite3_total_changes64(p->db)); |
| 21668 | } |
| 21669 | return 0; |
| 21670 | } |
| 21671 | |
| 21672 | |
| @@ -21683,14 +21833,14 @@ | |
| 21683 | char *zLine = 0; /* A single input line */ |
| 21684 | char *zSql = 0; /* Accumulated SQL text */ |
| 21685 | int nLine; /* Length of current line */ |
| 21686 | int nSql = 0; /* Bytes of zSql[] used */ |
| 21687 | int nAlloc = 0; /* Allocated zSql[] space */ |
| 21688 | int nSqlPrior = 0; /* Bytes of zSql[] used by prior line */ |
| 21689 | int rc; /* Error code */ |
| 21690 | int errCnt = 0; /* Number of errors seen */ |
| 21691 | int startline = 0; /* Line number for start of current input */ |
| 21692 | |
| 21693 | p->lineno = 0; |
| 21694 | while( errCnt==0 || !bail_on_error || (p->in==0 && stdin_is_interactive) ){ |
| 21695 | fflush(p->out); |
| 21696 | zLine = one_input_line(p->in, zLine, nSql>0); |
| @@ -21702,12 +21852,20 @@ | |
| 21702 | if( seenInterrupt ){ |
| 21703 | if( p->in!=0 ) break; |
| 21704 | seenInterrupt = 0; |
| 21705 | } |
| 21706 | p->lineno++; |
| 21707 | if( nSql==0 && _all_whitespace(zLine) ){ |
| 21708 | if( ShellHasFlag(p, SHFLG_Echo) ) printf("%s\n", zLine); |
| 21709 | continue; |
| 21710 | } |
| 21711 | if( zLine && (zLine[0]=='.' || zLine[0]=='#') && nSql==0 ){ |
| 21712 | if( ShellHasFlag(p, SHFLG_Echo) ) printf("%s\n", zLine); |
| 21713 | if( zLine[0]=='.' ){ |
| @@ -21718,20 +21876,17 @@ | |
| 21718 | errCnt++; |
| 21719 | } |
| 21720 | } |
| 21721 | continue; |
| 21722 | } |
| 21723 | if( line_is_command_terminator(zLine) && line_is_complete(zSql, nSql) ){ |
| 21724 | memcpy(zLine,";",2); |
| 21725 | } |
| 21726 | nLine = strlen30(zLine); |
| 21727 | if( nSql+nLine+2>=nAlloc ){ |
| 21728 | nAlloc = nSql+nLine+100; |
| 21729 | zSql = realloc(zSql, nAlloc); |
| 21730 | if( zSql==0 ) shell_out_of_memory(); |
| 21731 | } |
| 21732 | nSqlPrior = nSql; |
| 21733 | if( nSql==0 ){ |
| 21734 | int i; |
| 21735 | for(i=0; zLine[i] && IsSpace(zLine[i]); i++){} |
| 21736 | assert( nAlloc>0 && zSql!=0 ); |
| 21737 | memcpy(zSql, zLine+i, nLine+1-i); |
| @@ -21740,27 +21895,26 @@ | |
| 21740 | }else{ |
| 21741 | zSql[nSql++] = '\n'; |
| 21742 | memcpy(zSql+nSql, zLine, nLine+1); |
| 21743 | nSql += nLine; |
| 21744 | } |
| 21745 | if( nSql && line_contains_semicolon(&zSql[nSqlPrior], nSql-nSqlPrior) |
| 21746 | && sqlite3_complete(zSql) ){ |
| 21747 | errCnt += runOneSqlLine(p, zSql, p->in, startline); |
| 21748 | nSql = 0; |
| 21749 | if( p->outCount ){ |
| 21750 | output_reset(p); |
| 21751 | p->outCount = 0; |
| 21752 | }else{ |
| 21753 | clearTempFile(p); |
| 21754 | } |
| 21755 | p->bSafeMode = p->bSafeModePersist; |
| 21756 | }else if( nSql && _all_whitespace(zSql) ){ |
| 21757 | if( ShellHasFlag(p, SHFLG_Echo) ) printf("%s\n", zSql); |
| 21758 | nSql = 0; |
| 21759 | } |
| 21760 | } |
| 21761 | if( nSql && !_all_whitespace(zSql) ){ |
| 21762 | errCnt += runOneSqlLine(p, zSql, p->in, startline); |
| 21763 | } |
| 21764 | free(zSql); |
| 21765 | free(zLine); |
| 21766 | return errCnt>0; |
| @@ -22399,12 +22553,14 @@ | |
| 22399 | }else if( strcmp(z,"-nullvalue")==0 ){ |
| 22400 | sqlite3_snprintf(sizeof(data.nullValue), data.nullValue, |
| 22401 | "%s",cmdline_option_value(argc,argv,++i)); |
| 22402 | }else if( strcmp(z,"-header")==0 ){ |
| 22403 | data.showHeader = 1; |
| 22404 | }else if( strcmp(z,"-noheader")==0 ){ |
| 22405 | data.showHeader = 0; |
| 22406 | }else if( strcmp(z,"-echo")==0 ){ |
| 22407 | ShellSetFlag(&data, SHFLG_Echo); |
| 22408 | }else if( strcmp(z,"-eqp")==0 ){ |
| 22409 | data.autoEQP = AUTOEQP_on; |
| 22410 | }else if( strcmp(z,"-eqpfull")==0 ){ |
| 22411 |
| --- src/shell.c | |
| +++ src/shell.c | |
| @@ -651,23 +651,42 @@ | |
| 651 | } |
| 652 | return n; |
| 653 | } |
| 654 | |
| 655 | /* |
| 656 | ** Return open FILE * if zFile exists, can be opened for read |
| 657 | ** and is an ordinary file or a character stream source. |
| 658 | ** Otherwise return 0. |
| 659 | */ |
| 660 | static FILE * openChrSource(const char *zFile){ |
| 661 | #ifdef _WIN32 |
| 662 | struct _stat x = {0}; |
| 663 | # define STAT_CHR_SRC(mode) ((mode & (_S_IFCHR|_S_IFIFO|_S_IFREG))!=0) |
| 664 | /* On Windows, open first, then check the stream nature. This order |
| 665 | ** is necessary because _stat() and sibs, when checking a named pipe, |
| 666 | ** effectively break the pipe as its supplier sees it. */ |
| 667 | FILE *rv = fopen(zFile, "rb"); |
| 668 | if( rv==0 ) return 0; |
| 669 | if( _fstat(_fileno(rv), &x) != 0 |
| 670 | || !STAT_CHR_SRC(x.st_mode)){ |
| 671 | fclose(rv); |
| 672 | rv = 0; |
| 673 | } |
| 674 | return rv; |
| 675 | #else |
| 676 | struct stat x = {0}; |
| 677 | int rc = stat(zFile, &x); |
| 678 | # define STAT_CHR_SRC(mode) (S_ISREG(mode)||S_ISFIFO(mode)||S_ISCHR(mode)) |
| 679 | if( rc!=0 ) return 0; |
| 680 | if( STAT_CHR_SRC(x.st_mode) ){ |
| 681 | return fopen(zFile, "rb"); |
| 682 | }else{ |
| 683 | return 0; |
| 684 | } |
| 685 | #endif |
| 686 | #undef STAT_CHR_SRC |
| 687 | } |
| 688 | |
| 689 | /* |
| 690 | ** This routine reads a line of text from FILE in, stores |
| 691 | ** the text in memory obtained from malloc() and returns a pointer |
| 692 | ** to the text. NULL is returned at end of file, or if malloc() |
| @@ -2202,10 +2221,15 @@ | |
| 2221 | ** |
| 2222 | ** If a non-NULL value is specified for the optional $dir parameter and |
| 2223 | ** $path is a relative path, then $path is interpreted relative to $dir. |
| 2224 | ** And the paths returned in the "name" column of the table are also |
| 2225 | ** relative to directory $dir. |
| 2226 | ** |
| 2227 | ** Notes on building this extension for Windows: |
| 2228 | ** Unless linked statically with the SQLite library, a preprocessor |
| 2229 | ** symbol, FILEIO_WIN32_DLL, must be #define'd to create a stand-alone |
| 2230 | ** DLL form of this extension for WIN32. See its use below for details. |
| 2231 | */ |
| 2232 | /* #include "sqlite3ext.h" */ |
| 2233 | SQLITE_EXTENSION_INIT1 |
| 2234 | #include <stdio.h> |
| 2235 | #include <string.h> |
| @@ -2355,10 +2379,26 @@ | |
| 2379 | fileIntervals.HighPart = pFileTime->dwHighDateTime; |
| 2380 | |
| 2381 | return (fileIntervals.QuadPart - epochIntervals.QuadPart) / 10000000; |
| 2382 | } |
| 2383 | |
| 2384 | |
| 2385 | #if defined(FILEIO_WIN32_DLL) && (defined(_WIN32) || defined(WIN32)) |
| 2386 | # /* To allow a standalone DLL, use this next replacement function: */ |
| 2387 | # undef sqlite3_win32_utf8_to_unicode |
| 2388 | # define sqlite3_win32_utf8_to_unicode utf8_to_utf16 |
| 2389 | # |
| 2390 | LPWSTR utf8_to_utf16(const char *z){ |
| 2391 | int nAllot = MultiByteToWideChar(CP_UTF8, 0, z, -1, NULL, 0); |
| 2392 | LPWSTR rv = sqlite3_malloc(nAllot * sizeof(WCHAR)); |
| 2393 | if( rv!=0 && 0 < MultiByteToWideChar(CP_UTF8, 0, z, -1, rv, nAllot) ) |
| 2394 | return rv; |
| 2395 | sqlite3_free(rv); |
| 2396 | return 0; |
| 2397 | } |
| 2398 | #endif |
| 2399 | |
| 2400 | /* |
| 2401 | ** This function attempts to normalize the time values found in the stat() |
| 2402 | ** buffer to UTC. This is necessary on Win32, where the runtime library |
| 2403 | ** appears to return these values as local times. |
| 2404 | */ |
| @@ -3128,10 +3168,18 @@ | |
| 3168 | if( rc==SQLITE_OK ){ |
| 3169 | rc = fsdirRegister(db); |
| 3170 | } |
| 3171 | return rc; |
| 3172 | } |
| 3173 | |
| 3174 | #if defined(FILEIO_WIN32_DLL) && (defined(_WIN32) || defined(WIN32)) |
| 3175 | /* To allow a standalone DLL, make test_windirent.c use the same |
| 3176 | * redefined SQLite API calls as the above extension code does. |
| 3177 | * Just pull in this .c to accomplish this. As a beneficial side |
| 3178 | * effect, this extension becomes a single translation unit. */ |
| 3179 | # include "test_windirent.c" |
| 3180 | #endif |
| 3181 | |
| 3182 | /************************* End ../ext/misc/fileio.c ********************/ |
| 3183 | /************************* Begin ../ext/misc/completion.c ******************/ |
| 3184 | /* |
| 3185 | ** 2017-07-10 |
| @@ -10099,10 +10147,23 @@ | |
| 10147 | idxFinalize(&rc, pIdxList); |
| 10148 | |
| 10149 | *pRc = rc; |
| 10150 | return 0; |
| 10151 | } |
| 10152 | |
| 10153 | /* Callback for sqlite3_exec() with query with leading count(*) column. |
| 10154 | * The first argument is expected to be an int*, referent to be incremented |
| 10155 | * if that leading column is not exactly '0'. |
| 10156 | */ |
| 10157 | static int countNonzeros(void* pCount, int nc, |
| 10158 | char* azResults[], char* azColumns[]){ |
| 10159 | (void)azColumns; /* Suppress unused parameter warning */ |
| 10160 | if( nc>0 && (azResults[0][0]!='0' || azResults[0][1]!=0) ){ |
| 10161 | *((int *)pCount) += 1; |
| 10162 | } |
| 10163 | return 0; |
| 10164 | } |
| 10165 | |
| 10166 | static int idxCreateFromCons( |
| 10167 | sqlite3expert *p, |
| 10168 | IdxScan *pScan, |
| 10169 | IdxConstraint *pEq, |
| @@ -10126,30 +10187,57 @@ | |
| 10187 | } |
| 10188 | |
| 10189 | if( rc==SQLITE_OK ){ |
| 10190 | /* Hash the list of columns to come up with a name for the index */ |
| 10191 | const char *zTable = pScan->pTab->zName; |
| 10192 | int quoteTable = idxIdentifierRequiresQuotes(zTable); |
| 10193 | char *zName = 0; /* Index name */ |
| 10194 | int collisions = 0; |
| 10195 | do{ |
| 10196 | int i; |
| 10197 | char *zFind; |
| 10198 | for(i=0; zCols[i]; i++){ |
| 10199 | h += ((h<<3) + zCols[i]); |
| 10200 | } |
| 10201 | sqlite3_free(zName); |
| 10202 | zName = sqlite3_mprintf("%s_idx_%08x", zTable, h); |
| 10203 | if( zName==0 ) break; |
| 10204 | /* Is is unique among table, view and index names? */ |
| 10205 | zFmt = "SELECT count(*) FROM sqlite_schema WHERE name=%Q" |
| 10206 | " AND type in ('index','table','view')"; |
| 10207 | zFind = sqlite3_mprintf(zFmt, zName); |
| 10208 | i = 0; |
| 10209 | rc = sqlite3_exec(dbm, zFind, countNonzeros, &i, 0); |
| 10210 | assert(rc==SQLITE_OK); |
| 10211 | sqlite3_free(zFind); |
| 10212 | if( i==0 ){ |
| 10213 | collisions = 0; |
| 10214 | break; |
| 10215 | } |
| 10216 | ++collisions; |
| 10217 | }while( collisions<50 && zName!=0 ); |
| 10218 | if( collisions ){ |
| 10219 | /* This return means "Gave up trying to find a unique index name." */ |
| 10220 | rc = SQLITE_BUSY_TIMEOUT; |
| 10221 | }else if( zName==0 ){ |
| 10222 | rc = SQLITE_NOMEM; |
| 10223 | }else{ |
| 10224 | if( quoteTable ){ |
| 10225 | zFmt = "CREATE INDEX \"%w\" ON \"%w\"(%s)"; |
| 10226 | }else{ |
| 10227 | zFmt = "CREATE INDEX %s ON %s(%s)"; |
| 10228 | } |
| 10229 | zIdx = sqlite3_mprintf(zFmt, zName, zTable, zCols); |
| 10230 | if( !zIdx ){ |
| 10231 | rc = SQLITE_NOMEM; |
| 10232 | }else{ |
| 10233 | rc = sqlite3_exec(dbm, zIdx, 0, 0, p->pzErrmsg); |
| 10234 | if( rc!=SQLITE_OK ){ |
| 10235 | rc = SQLITE_BUSY_TIMEOUT; |
| 10236 | }else{ |
| 10237 | idxHashAdd(&rc, &p->hIdx, zName, zIdx); |
| 10238 | } |
| 10239 | } |
| 10240 | sqlite3_free(zName); |
| 10241 | sqlite3_free(zIdx); |
| 10242 | } |
| 10243 | } |
| @@ -11069,10 +11157,14 @@ | |
| 11157 | rc = idxProcessTriggers(p, pzErr); |
| 11158 | |
| 11159 | /* Create candidate indexes within the in-memory database file */ |
| 11160 | if( rc==SQLITE_OK ){ |
| 11161 | rc = idxCreateCandidates(p); |
| 11162 | }else if ( rc==SQLITE_BUSY_TIMEOUT ){ |
| 11163 | if( pzErr ) |
| 11164 | *pzErr = sqlite3_mprintf("Cannot find a unique index name to propose."); |
| 11165 | return rc; |
| 11166 | } |
| 11167 | |
| 11168 | /* Generate the stat1 data */ |
| 11169 | if( rc==SQLITE_OK ){ |
| 11170 | rc = idxPopulateStat1(p, pzErr); |
| @@ -12161,11 +12253,11 @@ | |
| 12253 | #define SHFLG_Backslash 0x00000004 /* The --backslash option is used */ |
| 12254 | #define SHFLG_PreserveRowid 0x00000008 /* .dump preserves rowid values */ |
| 12255 | #define SHFLG_Newlines 0x00000010 /* .dump --newline flag */ |
| 12256 | #define SHFLG_CountChanges 0x00000020 /* .changes setting */ |
| 12257 | #define SHFLG_Echo 0x00000040 /* .echo or --echo setting */ |
| 12258 | #define SHFLG_HeaderSet 0x00000080 /* showHeader has been specified */ |
| 12259 | #define SHFLG_DumpDataOnly 0x00000100 /* .dump show data only */ |
| 12260 | #define SHFLG_DumpNoSys 0x00000200 /* .dump omits system tables */ |
| 12261 | |
| 12262 | /* |
| 12263 | ** Macros for testing and setting shellFlgs |
| @@ -20197,11 +20289,11 @@ | |
| 20289 | }else{ |
| 20290 | rc = process_input(p); |
| 20291 | pclose(p->in); |
| 20292 | } |
| 20293 | #endif |
| 20294 | }else if( (p->in = openChrSource(azArg[1]))==0 ){ |
| 20295 | utf8_printf(stderr,"Error: cannot open \"%s\"\n", azArg[1]); |
| 20296 | rc = 1; |
| 20297 | }else{ |
| 20298 | rc = process_input(p); |
| 20299 | fclose(p->in); |
| @@ -21556,59 +21648,114 @@ | |
| 21648 | } |
| 21649 | p->bSafeMode = p->bSafeModePersist; |
| 21650 | return rc; |
| 21651 | } |
| 21652 | |
| 21653 | /* Line scan result and intermediate states (supporting scan resumption) |
| 21654 | */ |
| 21655 | #ifndef CHAR_BIT |
| 21656 | # define CHAR_BIT 8 |
| 21657 | #endif |
| 21658 | typedef enum { |
| 21659 | QSS_HasDark = 1<<CHAR_BIT, QSS_EndingSemi = 2<<CHAR_BIT, |
| 21660 | QSS_CharMask = (1<<CHAR_BIT)-1, QSS_ScanMask = 3<<CHAR_BIT, |
| 21661 | QSS_Start = 0 |
| 21662 | } QuickScanState; |
| 21663 | #define QSS_SETV(qss, newst) ((newst) | ((qss) & QSS_ScanMask)) |
| 21664 | #define QSS_INPLAIN(qss) (((qss)&QSS_CharMask)==QSS_Start) |
| 21665 | #define QSS_PLAINWHITE(qss) (((qss)&~QSS_EndingSemi)==QSS_Start) |
| 21666 | #define QSS_PLAINDARK(qss) (((qss)&~QSS_EndingSemi)==QSS_HasDark) |
| 21667 | #define QSS_SEMITERM(qss) (((qss)&~QSS_HasDark)==QSS_EndingSemi) |
| 21668 | |
| 21669 | /* |
| 21670 | ** Scan line for classification to guide shell's handling. |
| 21671 | ** The scan is resumable for subsequent lines when prior |
| 21672 | ** return values are passed as the 2nd argument. |
| 21673 | */ |
| 21674 | static QuickScanState quickscan(char *zLine, QuickScanState qss){ |
| 21675 | char cin; |
| 21676 | char cWait = (char)qss; /* intentional narrowing loss */ |
| 21677 | if( cWait==0 ){ |
| 21678 | PlainScan: |
| 21679 | while( (cin = *zLine++)!=0 ){ |
| 21680 | if( IsSpace(cin) ) |
| 21681 | continue; |
| 21682 | switch (cin){ |
| 21683 | case '-': |
| 21684 | if( *zLine!='-' ) |
| 21685 | break; |
| 21686 | while((cin = *++zLine)!=0 ) |
| 21687 | if( cin=='\n') |
| 21688 | goto PlainScan; |
| 21689 | return qss; |
| 21690 | case ';': |
| 21691 | qss |= QSS_EndingSemi; |
| 21692 | continue; |
| 21693 | case '/': |
| 21694 | if( *zLine=='*' ){ |
| 21695 | ++zLine; |
| 21696 | cWait = '*'; |
| 21697 | qss = QSS_SETV(qss, cWait); |
| 21698 | goto TermScan; |
| 21699 | } |
| 21700 | break; |
| 21701 | case '[': |
| 21702 | cin = ']'; |
| 21703 | /* fall thru */ |
| 21704 | case '`': case '\'': case '"': |
| 21705 | cWait = cin; |
| 21706 | qss = QSS_HasDark | cWait; |
| 21707 | goto TermScan; |
| 21708 | default: |
| 21709 | break; |
| 21710 | } |
| 21711 | qss = (qss & ~QSS_EndingSemi) | QSS_HasDark; |
| 21712 | } |
| 21713 | }else{ |
| 21714 | TermScan: |
| 21715 | while( (cin = *zLine++)!=0 ){ |
| 21716 | if( cin==cWait ){ |
| 21717 | switch( cWait ){ |
| 21718 | case '*': |
| 21719 | if( *zLine != '/' ) |
| 21720 | continue; |
| 21721 | ++zLine; |
| 21722 | cWait = 0; |
| 21723 | qss = QSS_SETV(qss, 0); |
| 21724 | goto PlainScan; |
| 21725 | case '`': case '\'': case '"': |
| 21726 | if(*zLine==cWait){ |
| 21727 | ++zLine; |
| 21728 | continue; |
| 21729 | } |
| 21730 | /* fall thru */ |
| 21731 | case ']': |
| 21732 | cWait = 0; |
| 21733 | qss = QSS_SETV(qss, 0); |
| 21734 | goto PlainScan; |
| 21735 | default: assert(0); |
| 21736 | } |
| 21737 | } |
| 21738 | } |
| 21739 | } |
| 21740 | return qss; |
| 21741 | } |
| 21742 | |
| 21743 | /* |
| 21744 | ** Return TRUE if the line typed in is an SQL command terminator other |
| 21745 | ** than a semi-colon. The SQL Server style "go" command is understood |
| 21746 | ** as is the Oracle "/". |
| 21747 | */ |
| 21748 | static int line_is_command_terminator(char *zLine){ |
| 21749 | while( IsSpace(zLine[0]) ){ zLine++; }; |
| 21750 | if( zLine[0]=='/' ) |
| 21751 | zLine += 1; /* Oracle */ |
| 21752 | else if ( ToLower(zLine[0])=='g' && ToLower(zLine[1])=='o' ) |
| 21753 | zLine += 2; /* SQL Server */ |
| 21754 | else |
| 21755 | return 0; |
| 21756 | return quickscan(zLine,QSS_Start)==QSS_Start; |
| 21757 | } |
| 21758 | |
| 21759 | /* |
| 21760 | ** We need a default sqlite3_complete() implementation to use in case |
| 21761 | ** the shell is compiled with SQLITE_OMIT_COMPLETE. The default assumes |
| @@ -21661,12 +21808,15 @@ | |
| 21808 | }else{ |
| 21809 | utf8_printf(stderr, "%s %s\n", zPrefix, sqlite3_errmsg(p->db)); |
| 21810 | } |
| 21811 | return 1; |
| 21812 | }else if( ShellHasFlag(p, SHFLG_CountChanges) ){ |
| 21813 | char zLineBuf[2000]; |
| 21814 | sqlite3_snprintf(sizeof(zLineBuf), zLineBuf, |
| 21815 | "changes: %lld total_changes: %lld", |
| 21816 | sqlite3_changes64(p->db), sqlite3_total_changes64(p->db)); |
| 21817 | raw_printf(p->out, "%s\n", zLineBuf); |
| 21818 | } |
| 21819 | return 0; |
| 21820 | } |
| 21821 | |
| 21822 | |
| @@ -21683,14 +21833,14 @@ | |
| 21833 | char *zLine = 0; /* A single input line */ |
| 21834 | char *zSql = 0; /* Accumulated SQL text */ |
| 21835 | int nLine; /* Length of current line */ |
| 21836 | int nSql = 0; /* Bytes of zSql[] used */ |
| 21837 | int nAlloc = 0; /* Allocated zSql[] space */ |
| 21838 | int rc; /* Error code */ |
| 21839 | int errCnt = 0; /* Number of errors seen */ |
| 21840 | int startline = 0; /* Line number for start of current input */ |
| 21841 | QuickScanState qss = QSS_Start; /* Accumulated line status (so far) */ |
| 21842 | |
| 21843 | p->lineno = 0; |
| 21844 | while( errCnt==0 || !bail_on_error || (p->in==0 && stdin_is_interactive) ){ |
| 21845 | fflush(p->out); |
| 21846 | zLine = one_input_line(p->in, zLine, nSql>0); |
| @@ -21702,12 +21852,20 @@ | |
| 21852 | if( seenInterrupt ){ |
| 21853 | if( p->in!=0 ) break; |
| 21854 | seenInterrupt = 0; |
| 21855 | } |
| 21856 | p->lineno++; |
| 21857 | if( QSS_INPLAIN(qss) |
| 21858 | && line_is_command_terminator(zLine) |
| 21859 | && line_is_complete(zSql, nSql) ){ |
| 21860 | memcpy(zLine,";",2); |
| 21861 | } |
| 21862 | qss = quickscan(zLine, qss); |
| 21863 | if( QSS_PLAINWHITE(qss) && nSql==0 ){ |
| 21864 | if( ShellHasFlag(p, SHFLG_Echo) ) |
| 21865 | printf("%s\n", zLine); |
| 21866 | /* Just swallow leading whitespace */ |
| 21867 | continue; |
| 21868 | } |
| 21869 | if( zLine && (zLine[0]=='.' || zLine[0]=='#') && nSql==0 ){ |
| 21870 | if( ShellHasFlag(p, SHFLG_Echo) ) printf("%s\n", zLine); |
| 21871 | if( zLine[0]=='.' ){ |
| @@ -21718,20 +21876,17 @@ | |
| 21876 | errCnt++; |
| 21877 | } |
| 21878 | } |
| 21879 | continue; |
| 21880 | } |
| 21881 | nLine = strlen30(zLine); |
| 21882 | if( nSql+nLine+2>=nAlloc ){ |
| 21883 | /* Grow buffer by half-again increments when big. */ |
| 21884 | nAlloc = nSql+(nSql>>1)+nLine+100; |
| 21885 | zSql = realloc(zSql, nAlloc); |
| 21886 | if( zSql==0 ) shell_out_of_memory(); |
| 21887 | } |
| 21888 | if( nSql==0 ){ |
| 21889 | int i; |
| 21890 | for(i=0; zLine[i] && IsSpace(zLine[i]); i++){} |
| 21891 | assert( nAlloc>0 && zSql!=0 ); |
| 21892 | memcpy(zSql, zLine+i, nLine+1-i); |
| @@ -21740,27 +21895,26 @@ | |
| 21895 | }else{ |
| 21896 | zSql[nSql++] = '\n'; |
| 21897 | memcpy(zSql+nSql, zLine, nLine+1); |
| 21898 | nSql += nLine; |
| 21899 | } |
| 21900 | if( nSql && QSS_SEMITERM(qss) && sqlite3_complete(zSql) ){ |
| 21901 | errCnt += runOneSqlLine(p, zSql, p->in, startline); |
| 21902 | nSql = 0; |
| 21903 | if( p->outCount ){ |
| 21904 | output_reset(p); |
| 21905 | p->outCount = 0; |
| 21906 | }else{ |
| 21907 | clearTempFile(p); |
| 21908 | } |
| 21909 | p->bSafeMode = p->bSafeModePersist; |
| 21910 | }else if( nSql && QSS_PLAINWHITE(qss) ){ |
| 21911 | if( ShellHasFlag(p, SHFLG_Echo) ) printf("%s\n", zSql); |
| 21912 | nSql = 0; |
| 21913 | } |
| 21914 | } |
| 21915 | if( nSql && QSS_PLAINDARK(qss) ){ |
| 21916 | errCnt += runOneSqlLine(p, zSql, p->in, startline); |
| 21917 | } |
| 21918 | free(zSql); |
| 21919 | free(zLine); |
| 21920 | return errCnt>0; |
| @@ -22399,12 +22553,14 @@ | |
| 22553 | }else if( strcmp(z,"-nullvalue")==0 ){ |
| 22554 | sqlite3_snprintf(sizeof(data.nullValue), data.nullValue, |
| 22555 | "%s",cmdline_option_value(argc,argv,++i)); |
| 22556 | }else if( strcmp(z,"-header")==0 ){ |
| 22557 | data.showHeader = 1; |
| 22558 | ShellSetFlag(&data, SHFLG_HeaderSet); |
| 22559 | }else if( strcmp(z,"-noheader")==0 ){ |
| 22560 | data.showHeader = 0; |
| 22561 | ShellSetFlag(&data, SHFLG_HeaderSet); |
| 22562 | }else if( strcmp(z,"-echo")==0 ){ |
| 22563 | ShellSetFlag(&data, SHFLG_Echo); |
| 22564 | }else if( strcmp(z,"-eqp")==0 ){ |
| 22565 | data.autoEQP = AUTOEQP_on; |
| 22566 | }else if( strcmp(z,"-eqpfull")==0 ){ |
| 22567 |
+257
-137
| --- src/sqlite3.c | ||
| +++ src/sqlite3.c | ||
| @@ -452,11 +452,11 @@ | ||
| 452 | 452 | ** [sqlite3_libversion_number()], [sqlite3_sourceid()], |
| 453 | 453 | ** [sqlite_version()] and [sqlite_source_id()]. |
| 454 | 454 | */ |
| 455 | 455 | #define SQLITE_VERSION "3.37.0" |
| 456 | 456 | #define SQLITE_VERSION_NUMBER 3037000 |
| 457 | -#define SQLITE_SOURCE_ID "2021-09-06 11:44:19 b3cfe23bec0b95ca673802526704200e2396df715fdded72aa71addd7f47e0e1" | |
| 457 | +#define SQLITE_SOURCE_ID "2021-09-22 14:43:35 d678ecca02698753d1b33e072566112e94ea36d0d3a8f4a24d2b09d131968d88" | |
| 458 | 458 | |
| 459 | 459 | /* |
| 460 | 460 | ** CAPI3REF: Run-Time Library Version Numbers |
| 461 | 461 | ** KEYWORDS: sqlite3_version sqlite3_sourceid |
| 462 | 462 | ** |
| @@ -19842,11 +19842,11 @@ | ||
| 19842 | 19842 | #ifdef SQLITE_TEST |
| 19843 | 19843 | SQLITE_PRIVATE int sqlite3Utf8To8(unsigned char*); |
| 19844 | 19844 | #endif |
| 19845 | 19845 | |
| 19846 | 19846 | #ifdef SQLITE_OMIT_VIRTUALTABLE |
| 19847 | -# define sqlite3VtabClear(Y) | |
| 19847 | +# define sqlite3VtabClear(D,T) | |
| 19848 | 19848 | # define sqlite3VtabSync(X,Y) SQLITE_OK |
| 19849 | 19849 | # define sqlite3VtabRollback(X) |
| 19850 | 19850 | # define sqlite3VtabCommit(X) |
| 19851 | 19851 | # define sqlite3VtabInSync(db) 0 |
| 19852 | 19852 | # define sqlite3VtabLock(X) |
| @@ -48856,11 +48856,11 @@ | ||
| 48856 | 48856 | MemStore *p = 0; |
| 48857 | 48857 | int szName; |
| 48858 | 48858 | if( (flags & SQLITE_OPEN_MAIN_DB)==0 ){ |
| 48859 | 48859 | return ORIGVFS(pVfs)->xOpen(ORIGVFS(pVfs), zName, pFd, flags, pOutFlags); |
| 48860 | 48860 | } |
| 48861 | - memset(pFile, 0, sizeof(*p)); | |
| 48861 | + memset(pFile, 0, sizeof(*pFile)); | |
| 48862 | 48862 | szName = sqlite3Strlen30(zName); |
| 48863 | 48863 | if( szName>1 && zName[0]=='/' ){ |
| 48864 | 48864 | int i; |
| 48865 | 48865 | #ifndef SQLITE_MUTEX_OMIT |
| 48866 | 48866 | sqlite3_mutex *pVfsMutex = sqlite3MutexAlloc(SQLITE_MUTEX_STATIC_VFS1); |
| @@ -53160,12 +53160,12 @@ | ||
| 53160 | 53160 | |
| 53161 | 53161 | u16 nExtra; /* Add this many bytes to each in-memory page */ |
| 53162 | 53162 | i16 nReserve; /* Number of unused bytes at end of each page */ |
| 53163 | 53163 | u32 vfsFlags; /* Flags for sqlite3_vfs.xOpen() */ |
| 53164 | 53164 | u32 sectorSize; /* Assumed sector size during rollback */ |
| 53165 | - int pageSize; /* Number of bytes in a page */ | |
| 53166 | 53165 | Pgno mxPgno; /* Maximum allowed size of the database */ |
| 53166 | + i64 pageSize; /* Number of bytes in a page */ | |
| 53167 | 53167 | i64 journalSizeLimit; /* Size limit for persistent journal files */ |
| 53168 | 53168 | char *zFilename; /* Name of the database file */ |
| 53169 | 53169 | char *zJournal; /* Name of the journal file */ |
| 53170 | 53170 | int (*xBusyHandler)(void*); /* Function to call when busy */ |
| 53171 | 53171 | void *pBusyHandlerArg; /* Context argument for xBusyHandler */ |
| @@ -59218,12 +59218,12 @@ | ||
| 59218 | 59218 | /* |
| 59219 | 59219 | ** Return the approximate number of bytes of memory currently |
| 59220 | 59220 | ** used by the pager and its associated cache. |
| 59221 | 59221 | */ |
| 59222 | 59222 | SQLITE_PRIVATE int sqlite3PagerMemUsed(Pager *pPager){ |
| 59223 | - int perPageSize = pPager->pageSize + pPager->nExtra + sizeof(PgHdr) | |
| 59224 | - + 5*sizeof(void*); | |
| 59223 | + int perPageSize = pPager->pageSize + pPager->nExtra | |
| 59224 | + + (int)(sizeof(PgHdr) + 5*sizeof(void*)); | |
| 59225 | 59225 | return perPageSize*sqlite3PcachePagecount(pPager->pPCache) |
| 59226 | 59226 | + sqlite3MallocSize(pPager) |
| 59227 | 59227 | + pPager->pageSize; |
| 59228 | 59228 | } |
| 59229 | 59229 | |
| @@ -59413,18 +59413,18 @@ | ||
| 59413 | 59413 | for(ii=nNew; ii<pPager->nSavepoint; ii++){ |
| 59414 | 59414 | sqlite3BitvecDestroy(pPager->aSavepoint[ii].pInSavepoint); |
| 59415 | 59415 | } |
| 59416 | 59416 | pPager->nSavepoint = nNew; |
| 59417 | 59417 | |
| 59418 | - /* If this is a release of the outermost savepoint, truncate | |
| 59419 | - ** the sub-journal to zero bytes in size. */ | |
| 59418 | + /* Truncate the sub-journal so that it only includes the parts | |
| 59419 | + ** that are still in use. */ | |
| 59420 | 59420 | if( op==SAVEPOINT_RELEASE ){ |
| 59421 | 59421 | PagerSavepoint *pRel = &pPager->aSavepoint[nNew]; |
| 59422 | 59422 | if( pRel->bTruncateOnRelease && isOpen(pPager->sjfd) ){ |
| 59423 | 59423 | /* Only truncate if it is an in-memory sub-journal. */ |
| 59424 | 59424 | if( sqlite3JournalIsInMemory(pPager->sjfd) ){ |
| 59425 | - i64 sz = (pPager->pageSize+4)*pRel->iSubRec; | |
| 59425 | + i64 sz = (pPager->pageSize+4)*(i64)pRel->iSubRec; | |
| 59426 | 59426 | rc = sqlite3OsTruncate(pPager->sjfd, sz); |
| 59427 | 59427 | assert( rc==SQLITE_OK ); |
| 59428 | 59428 | } |
| 59429 | 59429 | pPager->nSubRec = pRel->iSubRec; |
| 59430 | 59430 | } |
| @@ -72713,10 +72713,11 @@ | ||
| 72713 | 72713 | nCell -= nTail; |
| 72714 | 72714 | } |
| 72715 | 72715 | |
| 72716 | 72716 | pData = &aData[get2byteNotZero(&aData[hdr+5])]; |
| 72717 | 72717 | if( pData<pBegin ) goto editpage_fail; |
| 72718 | + if( NEVER(pData>pPg->aDataEnd) ) goto editpage_fail; | |
| 72718 | 72719 | |
| 72719 | 72720 | /* Add cells to the start of the page */ |
| 72720 | 72721 | if( iNew<iOld ){ |
| 72721 | 72722 | int nAdd = MIN(nNew,iOld-iNew); |
| 72722 | 72723 | assert( (iOld-iNew)<nNew || nCell==0 || CORRUPT_DB ); |
| @@ -74118,11 +74119,11 @@ | ||
| 74118 | 74119 | pBt = pPage->pBt; |
| 74119 | 74120 | ovflPageSize = pBt->usableSize - 4; |
| 74120 | 74121 | do{ |
| 74121 | 74122 | rc = btreeGetPage(pBt, ovflPgno, &pPage, 0); |
| 74122 | 74123 | if( rc ) return rc; |
| 74123 | - if( sqlite3PagerPageRefcount(pPage->pDbPage)!=1 ){ | |
| 74124 | + if( sqlite3PagerPageRefcount(pPage->pDbPage)!=1 || pPage->isInit ){ | |
| 74124 | 74125 | rc = SQLITE_CORRUPT_BKPT; |
| 74125 | 74126 | }else{ |
| 74126 | 74127 | if( iOffset+ovflPageSize<(u32)nTotal ){ |
| 74127 | 74128 | ovflPgno = get4byte(pPage->aData); |
| 74128 | 74129 | }else{ |
| @@ -94795,10 +94796,15 @@ | ||
| 94795 | 94796 | rc = SQLITE_NOMEM_BKPT; |
| 94796 | 94797 | }else if( rc==SQLITE_IOERR_CORRUPTFS ){ |
| 94797 | 94798 | rc = SQLITE_CORRUPT_BKPT; |
| 94798 | 94799 | } |
| 94799 | 94800 | assert( rc ); |
| 94801 | +#ifdef SQLITE_DEBUG | |
| 94802 | + if( db->flags & SQLITE_VdbeTrace ){ | |
| 94803 | + printf("ABORT-due-to-error. rc=%d\n", rc); | |
| 94804 | + } | |
| 94805 | +#endif | |
| 94800 | 94806 | if( p->zErrMsg==0 && rc!=SQLITE_IOERR_NOMEM ){ |
| 94801 | 94807 | sqlite3VdbeError(p, "%s", sqlite3ErrStr(rc)); |
| 94802 | 94808 | } |
| 94803 | 94809 | p->rc = rc; |
| 94804 | 94810 | sqlite3SystemError(db, rc); |
| @@ -105161,10 +105167,11 @@ | ||
| 105161 | 105167 | |
| 105162 | 105168 | /*********************************************************************** |
| 105163 | 105169 | ** Test-only SQL functions that are only usable if enabled |
| 105164 | 105170 | ** via SQLITE_TESTCTRL_INTERNAL_FUNCTIONS |
| 105165 | 105171 | */ |
| 105172 | +#if !defined(SQLITE_UNTESTABLE) | |
| 105166 | 105173 | case INLINEFUNC_expr_compare: { |
| 105167 | 105174 | /* Compare two expressions using sqlite3ExprCompare() */ |
| 105168 | 105175 | assert( nFarg==2 ); |
| 105169 | 105176 | sqlite3VdbeAddOp2(v, OP_Integer, |
| 105170 | 105177 | sqlite3ExprCompare(0,pFarg->a[0].pExpr, pFarg->a[1].pExpr,-1), |
| @@ -105194,11 +105201,10 @@ | ||
| 105194 | 105201 | sqlite3VdbeAddOp2(v, OP_Null, 0, target); |
| 105195 | 105202 | } |
| 105196 | 105203 | break; |
| 105197 | 105204 | } |
| 105198 | 105205 | |
| 105199 | -#ifdef SQLITE_DEBUG | |
| 105200 | 105206 | case INLINEFUNC_affinity: { |
| 105201 | 105207 | /* The AFFINITY() function evaluates to a string that describes |
| 105202 | 105208 | ** the type affinity of the argument. This is used for testing of |
| 105203 | 105209 | ** the SQLite type logic. |
| 105204 | 105210 | */ |
| @@ -105208,11 +105214,11 @@ | ||
| 105208 | 105214 | aff = sqlite3ExprAffinity(pFarg->a[0].pExpr); |
| 105209 | 105215 | sqlite3VdbeLoadString(v, target, |
| 105210 | 105216 | (aff<=SQLITE_AFF_NONE) ? "none" : azAff[aff-SQLITE_AFF_BLOB]); |
| 105211 | 105217 | break; |
| 105212 | 105218 | } |
| 105213 | -#endif | |
| 105219 | +#endif /* !defined(SQLITE_UNTESTABLE) */ | |
| 105214 | 105220 | } |
| 105215 | 105221 | return target; |
| 105216 | 105222 | } |
| 105217 | 105223 | |
| 105218 | 105224 | |
| @@ -108172,12 +108178,11 @@ | ||
| 108172 | 108178 | bQuote = sqlite3Isquote(pNew->z[0]); |
| 108173 | 108179 | sqlite3NestedParse(pParse, |
| 108174 | 108180 | "UPDATE \"%w\"." DFLT_SCHEMA_TABLE " SET " |
| 108175 | 108181 | "sql = sqlite_rename_column(sql, type, name, %Q, %Q, %d, %Q, %d, %d) " |
| 108176 | 108182 | "WHERE name NOT LIKE 'sqliteX_%%' ESCAPE 'X' " |
| 108177 | - " AND (type != 'index' OR tbl_name = %Q)" | |
| 108178 | - " AND sql NOT LIKE 'create virtual%%'", | |
| 108183 | + " AND (type != 'index' OR tbl_name = %Q)", | |
| 108179 | 108184 | zDb, |
| 108180 | 108185 | zDb, pTab->zName, iCol, zNew, bQuote, iSchema==1, |
| 108181 | 108186 | pTab->zName |
| 108182 | 108187 | ); |
| 108183 | 108188 | |
| @@ -109023,11 +109028,11 @@ | ||
| 109023 | 109028 | rc = (db->mallocFailed ? SQLITE_NOMEM : sParse.rc); |
| 109024 | 109029 | if( rc==SQLITE_OK ){ |
| 109025 | 109030 | sqlite3WalkSelect(&sWalker, pSelect); |
| 109026 | 109031 | } |
| 109027 | 109032 | if( rc!=SQLITE_OK ) goto renameColumnFunc_done; |
| 109028 | - }else if( ALWAYS(IsOrdinaryTable(sParse.pNewTable)) ){ | |
| 109033 | + }else if( IsOrdinaryTable(sParse.pNewTable) ){ | |
| 109029 | 109034 | /* A regular table */ |
| 109030 | 109035 | int bFKOnly = sqlite3_stricmp(zTable, sParse.pNewTable->zName); |
| 109031 | 109036 | FKey *pFKey; |
| 109032 | 109037 | sCtx.pTab = sParse.pNewTable; |
| 109033 | 109038 | if( bFKOnly==0 ){ |
| @@ -120209,13 +120214,13 @@ | ||
| 120209 | 120214 | } |
| 120210 | 120215 | |
| 120211 | 120216 | /* |
| 120212 | 120217 | ** Implementation of the changes() SQL function. |
| 120213 | 120218 | ** |
| 120214 | -** IMP: R-62073-11209 The changes() SQL function is a wrapper | |
| 120215 | -** around the sqlite3_changes64() C/C++ function and hence follows the same | |
| 120216 | -** rules for counting changes. | |
| 120219 | +** IMP: R-32760-32347 The changes() SQL function is a wrapper | |
| 120220 | +** around the sqlite3_changes64() C/C++ function and hence follows the | |
| 120221 | +** same rules for counting changes. | |
| 120217 | 120222 | */ |
| 120218 | 120223 | static void changes( |
| 120219 | 120224 | sqlite3_context *context, |
| 120220 | 120225 | int NotUsed, |
| 120221 | 120226 | sqlite3_value **NotUsed2 |
| @@ -120234,12 +120239,12 @@ | ||
| 120234 | 120239 | int NotUsed, |
| 120235 | 120240 | sqlite3_value **NotUsed2 |
| 120236 | 120241 | ){ |
| 120237 | 120242 | sqlite3 *db = sqlite3_context_db_handle(context); |
| 120238 | 120243 | UNUSED_PARAMETER2(NotUsed, NotUsed2); |
| 120239 | - /* IMP: R-52756-41993 This function was a wrapper around the | |
| 120240 | - ** sqlite3_total_changes() C/C++ interface. */ | |
| 120244 | + /* IMP: R-11217-42568 This function is a wrapper around the | |
| 120245 | + ** sqlite3_total_changes64() C/C++ interface. */ | |
| 120241 | 120246 | sqlite3_result_int64(context, sqlite3_total_changes64(db)); |
| 120242 | 120247 | } |
| 120243 | 120248 | |
| 120244 | 120249 | /* |
| 120245 | 120250 | ** A structure defining how to do GLOB-style comparisons. |
| @@ -121761,16 +121766,16 @@ | ||
| 121761 | 121766 | ** |
| 121762 | 121767 | ** For peak efficiency, put the most frequently used function last. |
| 121763 | 121768 | */ |
| 121764 | 121769 | static FuncDef aBuiltinFunc[] = { |
| 121765 | 121770 | /***** Functions only available with SQLITE_TESTCTRL_INTERNAL_FUNCTIONS *****/ |
| 121771 | +#if !defined(SQLITE_UNTESTABLE) | |
| 121766 | 121772 | TEST_FUNC(implies_nonnull_row, 2, INLINEFUNC_implies_nonnull_row, 0), |
| 121767 | 121773 | TEST_FUNC(expr_compare, 2, INLINEFUNC_expr_compare, 0), |
| 121768 | 121774 | TEST_FUNC(expr_implies_expr, 2, INLINEFUNC_expr_implies_expr, 0), |
| 121769 | -#ifdef SQLITE_DEBUG | |
| 121770 | - TEST_FUNC(affinity, 1, INLINEFUNC_affinity, 0), | |
| 121771 | -#endif | |
| 121775 | + TEST_FUNC(affinity, 1, INLINEFUNC_affinity, 0), | |
| 121776 | +#endif /* !defined(SQLITE_UNTESTABLE) */ | |
| 121772 | 121777 | /***** Regular functions *****/ |
| 121773 | 121778 | #ifdef SQLITE_SOUNDEX |
| 121774 | 121779 | FUNCTION(soundex, 1, 0, 0, soundexFunc ), |
| 121775 | 121780 | #endif |
| 121776 | 121781 | #ifndef SQLITE_OMIT_LOAD_EXTENSION |
| @@ -128264,17 +128269,18 @@ | ||
| 128264 | 128269 | #define PragTyp_SECURE_DELETE 33 |
| 128265 | 128270 | #define PragTyp_SHRINK_MEMORY 34 |
| 128266 | 128271 | #define PragTyp_SOFT_HEAP_LIMIT 35 |
| 128267 | 128272 | #define PragTyp_SYNCHRONOUS 36 |
| 128268 | 128273 | #define PragTyp_TABLE_INFO 37 |
| 128269 | -#define PragTyp_TEMP_STORE 38 | |
| 128270 | -#define PragTyp_TEMP_STORE_DIRECTORY 39 | |
| 128271 | -#define PragTyp_THREADS 40 | |
| 128272 | -#define PragTyp_WAL_AUTOCHECKPOINT 41 | |
| 128273 | -#define PragTyp_WAL_CHECKPOINT 42 | |
| 128274 | -#define PragTyp_LOCK_STATUS 43 | |
| 128275 | -#define PragTyp_STATS 44 | |
| 128274 | +#define PragTyp_TABLE_LIST 38 | |
| 128275 | +#define PragTyp_TEMP_STORE 39 | |
| 128276 | +#define PragTyp_TEMP_STORE_DIRECTORY 40 | |
| 128277 | +#define PragTyp_THREADS 41 | |
| 128278 | +#define PragTyp_WAL_AUTOCHECKPOINT 42 | |
| 128279 | +#define PragTyp_WAL_CHECKPOINT 43 | |
| 128280 | +#define PragTyp_LOCK_STATUS 44 | |
| 128281 | +#define PragTyp_STATS 45 | |
| 128276 | 128282 | |
| 128277 | 128283 | /* Property flags associated with various pragma. */ |
| 128278 | 128284 | #define PragFlg_NeedSchema 0x01 /* Force schema load before running */ |
| 128279 | 128285 | #define PragFlg_NoColumns 0x02 /* OP_ResultRow called with zero columns */ |
| 128280 | 128286 | #define PragFlg_NoColumns1 0x04 /* zero columns if RHS argument is present */ |
| @@ -128303,49 +128309,55 @@ | ||
| 128303 | 128309 | /* 11 */ "notnull", |
| 128304 | 128310 | /* 12 */ "dflt_value", |
| 128305 | 128311 | /* 13 */ "pk", |
| 128306 | 128312 | /* 14 */ "hidden", |
| 128307 | 128313 | /* table_info reuses 8 */ |
| 128308 | - /* 15 */ "seqno", /* Used by: index_xinfo */ | |
| 128309 | - /* 16 */ "cid", | |
| 128310 | - /* 17 */ "name", | |
| 128311 | - /* 18 */ "desc", | |
| 128312 | - /* 19 */ "coll", | |
| 128313 | - /* 20 */ "key", | |
| 128314 | - /* 21 */ "name", /* Used by: function_list */ | |
| 128315 | - /* 22 */ "builtin", | |
| 128316 | - /* 23 */ "type", | |
| 128317 | - /* 24 */ "enc", | |
| 128318 | - /* 25 */ "narg", | |
| 128319 | - /* 26 */ "flags", | |
| 128320 | - /* 27 */ "tbl", /* Used by: stats */ | |
| 128321 | - /* 28 */ "idx", | |
| 128322 | - /* 29 */ "wdth", | |
| 128323 | - /* 30 */ "hght", | |
| 128324 | - /* 31 */ "flgs", | |
| 128325 | - /* 32 */ "seq", /* Used by: index_list */ | |
| 128326 | - /* 33 */ "name", | |
| 128327 | - /* 34 */ "unique", | |
| 128328 | - /* 35 */ "origin", | |
| 128329 | - /* 36 */ "partial", | |
| 128330 | - /* 37 */ "table", /* Used by: foreign_key_check */ | |
| 128331 | - /* 38 */ "rowid", | |
| 128332 | - /* 39 */ "parent", | |
| 128333 | - /* 40 */ "fkid", | |
| 128334 | - /* index_info reuses 15 */ | |
| 128335 | - /* 41 */ "seq", /* Used by: database_list */ | |
| 128336 | - /* 42 */ "name", | |
| 128337 | - /* 43 */ "file", | |
| 128338 | - /* 44 */ "busy", /* Used by: wal_checkpoint */ | |
| 128339 | - /* 45 */ "log", | |
| 128340 | - /* 46 */ "checkpointed", | |
| 128341 | - /* collation_list reuses 32 */ | |
| 128342 | - /* 47 */ "database", /* Used by: lock_status */ | |
| 128343 | - /* 48 */ "status", | |
| 128344 | - /* 49 */ "cache_size", /* Used by: default_cache_size */ | |
| 128314 | + /* 15 */ "schema", /* Used by: table_list */ | |
| 128315 | + /* 16 */ "name", | |
| 128316 | + /* 17 */ "type", | |
| 128317 | + /* 18 */ "ncol", | |
| 128318 | + /* 19 */ "wr", | |
| 128319 | + /* 20 */ "strict", | |
| 128320 | + /* 21 */ "seqno", /* Used by: index_xinfo */ | |
| 128321 | + /* 22 */ "cid", | |
| 128322 | + /* 23 */ "name", | |
| 128323 | + /* 24 */ "desc", | |
| 128324 | + /* 25 */ "coll", | |
| 128325 | + /* 26 */ "key", | |
| 128326 | + /* 27 */ "name", /* Used by: function_list */ | |
| 128327 | + /* 28 */ "builtin", | |
| 128328 | + /* 29 */ "type", | |
| 128329 | + /* 30 */ "enc", | |
| 128330 | + /* 31 */ "narg", | |
| 128331 | + /* 32 */ "flags", | |
| 128332 | + /* 33 */ "tbl", /* Used by: stats */ | |
| 128333 | + /* 34 */ "idx", | |
| 128334 | + /* 35 */ "wdth", | |
| 128335 | + /* 36 */ "hght", | |
| 128336 | + /* 37 */ "flgs", | |
| 128337 | + /* 38 */ "seq", /* Used by: index_list */ | |
| 128338 | + /* 39 */ "name", | |
| 128339 | + /* 40 */ "unique", | |
| 128340 | + /* 41 */ "origin", | |
| 128341 | + /* 42 */ "partial", | |
| 128342 | + /* 43 */ "table", /* Used by: foreign_key_check */ | |
| 128343 | + /* 44 */ "rowid", | |
| 128344 | + /* 45 */ "parent", | |
| 128345 | + /* 46 */ "fkid", | |
| 128346 | + /* index_info reuses 21 */ | |
| 128347 | + /* 47 */ "seq", /* Used by: database_list */ | |
| 128348 | + /* 48 */ "name", | |
| 128349 | + /* 49 */ "file", | |
| 128350 | + /* 50 */ "busy", /* Used by: wal_checkpoint */ | |
| 128351 | + /* 51 */ "log", | |
| 128352 | + /* 52 */ "checkpointed", | |
| 128353 | + /* collation_list reuses 38 */ | |
| 128354 | + /* 53 */ "database", /* Used by: lock_status */ | |
| 128355 | + /* 54 */ "status", | |
| 128356 | + /* 55 */ "cache_size", /* Used by: default_cache_size */ | |
| 128345 | 128357 | /* module_list pragma_list reuses 9 */ |
| 128346 | - /* 50 */ "timeout", /* Used by: busy_timeout */ | |
| 128358 | + /* 56 */ "timeout", /* Used by: busy_timeout */ | |
| 128347 | 128359 | }; |
| 128348 | 128360 | |
| 128349 | 128361 | /* Definitions of all built-in pragmas */ |
| 128350 | 128362 | typedef struct PragmaName { |
| 128351 | 128363 | const char *const zName; /* Name of pragma */ |
| @@ -128392,11 +128404,11 @@ | ||
| 128392 | 128404 | #endif |
| 128393 | 128405 | #endif |
| 128394 | 128406 | {/* zName: */ "busy_timeout", |
| 128395 | 128407 | /* ePragTyp: */ PragTyp_BUSY_TIMEOUT, |
| 128396 | 128408 | /* ePragFlg: */ PragFlg_Result0, |
| 128397 | - /* ColNames: */ 50, 1, | |
| 128409 | + /* ColNames: */ 56, 1, | |
| 128398 | 128410 | /* iArg: */ 0 }, |
| 128399 | 128411 | #if !defined(SQLITE_OMIT_PAGER_PRAGMAS) |
| 128400 | 128412 | {/* zName: */ "cache_size", |
| 128401 | 128413 | /* ePragTyp: */ PragTyp_CACHE_SIZE, |
| 128402 | 128414 | /* ePragFlg: */ PragFlg_NeedSchema|PragFlg_Result0|PragFlg_SchemaReq|PragFlg_NoColumns1, |
| @@ -128431,11 +128443,11 @@ | ||
| 128431 | 128443 | #endif |
| 128432 | 128444 | #if !defined(SQLITE_OMIT_SCHEMA_PRAGMAS) |
| 128433 | 128445 | {/* zName: */ "collation_list", |
| 128434 | 128446 | /* ePragTyp: */ PragTyp_COLLATION_LIST, |
| 128435 | 128447 | /* ePragFlg: */ PragFlg_Result0, |
| 128436 | - /* ColNames: */ 32, 2, | |
| 128448 | + /* ColNames: */ 38, 2, | |
| 128437 | 128449 | /* iArg: */ 0 }, |
| 128438 | 128450 | #endif |
| 128439 | 128451 | #if !defined(SQLITE_OMIT_COMPILEOPTION_DIAGS) |
| 128440 | 128452 | {/* zName: */ "compile_options", |
| 128441 | 128453 | /* ePragTyp: */ PragTyp_COMPILE_OPTIONS, |
| @@ -128466,18 +128478,18 @@ | ||
| 128466 | 128478 | #endif |
| 128467 | 128479 | #if !defined(SQLITE_OMIT_SCHEMA_PRAGMAS) |
| 128468 | 128480 | {/* zName: */ "database_list", |
| 128469 | 128481 | /* ePragTyp: */ PragTyp_DATABASE_LIST, |
| 128470 | 128482 | /* ePragFlg: */ PragFlg_NeedSchema|PragFlg_Result0, |
| 128471 | - /* ColNames: */ 41, 3, | |
| 128483 | + /* ColNames: */ 47, 3, | |
| 128472 | 128484 | /* iArg: */ 0 }, |
| 128473 | 128485 | #endif |
| 128474 | 128486 | #if !defined(SQLITE_OMIT_PAGER_PRAGMAS) && !defined(SQLITE_OMIT_DEPRECATED) |
| 128475 | 128487 | {/* zName: */ "default_cache_size", |
| 128476 | 128488 | /* ePragTyp: */ PragTyp_DEFAULT_CACHE_SIZE, |
| 128477 | 128489 | /* ePragFlg: */ PragFlg_NeedSchema|PragFlg_Result0|PragFlg_SchemaReq|PragFlg_NoColumns1, |
| 128478 | - /* ColNames: */ 49, 1, | |
| 128490 | + /* ColNames: */ 55, 1, | |
| 128479 | 128491 | /* iArg: */ 0 }, |
| 128480 | 128492 | #endif |
| 128481 | 128493 | #if !defined(SQLITE_OMIT_FLAG_PRAGMAS) |
| 128482 | 128494 | #if !defined(SQLITE_OMIT_FOREIGN_KEY) && !defined(SQLITE_OMIT_TRIGGER) |
| 128483 | 128495 | {/* zName: */ "defer_foreign_keys", |
| @@ -128503,11 +128515,11 @@ | ||
| 128503 | 128515 | #endif |
| 128504 | 128516 | #if !defined(SQLITE_OMIT_FOREIGN_KEY) && !defined(SQLITE_OMIT_TRIGGER) |
| 128505 | 128517 | {/* zName: */ "foreign_key_check", |
| 128506 | 128518 | /* ePragTyp: */ PragTyp_FOREIGN_KEY_CHECK, |
| 128507 | 128519 | /* ePragFlg: */ PragFlg_NeedSchema|PragFlg_Result0|PragFlg_Result1|PragFlg_SchemaOpt, |
| 128508 | - /* ColNames: */ 37, 4, | |
| 128520 | + /* ColNames: */ 43, 4, | |
| 128509 | 128521 | /* iArg: */ 0 }, |
| 128510 | 128522 | #endif |
| 128511 | 128523 | #if !defined(SQLITE_OMIT_FOREIGN_KEY) |
| 128512 | 128524 | {/* zName: */ "foreign_key_list", |
| 128513 | 128525 | /* ePragTyp: */ PragTyp_FOREIGN_KEY_LIST, |
| @@ -128546,11 +128558,11 @@ | ||
| 128546 | 128558 | #if !defined(SQLITE_OMIT_SCHEMA_PRAGMAS) |
| 128547 | 128559 | #if !defined(SQLITE_OMIT_INTROSPECTION_PRAGMAS) |
| 128548 | 128560 | {/* zName: */ "function_list", |
| 128549 | 128561 | /* ePragTyp: */ PragTyp_FUNCTION_LIST, |
| 128550 | 128562 | /* ePragFlg: */ PragFlg_Result0, |
| 128551 | - /* ColNames: */ 21, 6, | |
| 128563 | + /* ColNames: */ 27, 6, | |
| 128552 | 128564 | /* iArg: */ 0 }, |
| 128553 | 128565 | #endif |
| 128554 | 128566 | #endif |
| 128555 | 128567 | {/* zName: */ "hard_heap_limit", |
| 128556 | 128568 | /* ePragTyp: */ PragTyp_HARD_HEAP_LIMIT, |
| @@ -128575,21 +128587,21 @@ | ||
| 128575 | 128587 | #endif |
| 128576 | 128588 | #if !defined(SQLITE_OMIT_SCHEMA_PRAGMAS) |
| 128577 | 128589 | {/* zName: */ "index_info", |
| 128578 | 128590 | /* ePragTyp: */ PragTyp_INDEX_INFO, |
| 128579 | 128591 | /* ePragFlg: */ PragFlg_NeedSchema|PragFlg_Result1|PragFlg_SchemaOpt, |
| 128580 | - /* ColNames: */ 15, 3, | |
| 128592 | + /* ColNames: */ 21, 3, | |
| 128581 | 128593 | /* iArg: */ 0 }, |
| 128582 | 128594 | {/* zName: */ "index_list", |
| 128583 | 128595 | /* ePragTyp: */ PragTyp_INDEX_LIST, |
| 128584 | 128596 | /* ePragFlg: */ PragFlg_NeedSchema|PragFlg_Result1|PragFlg_SchemaOpt, |
| 128585 | - /* ColNames: */ 32, 5, | |
| 128597 | + /* ColNames: */ 38, 5, | |
| 128586 | 128598 | /* iArg: */ 0 }, |
| 128587 | 128599 | {/* zName: */ "index_xinfo", |
| 128588 | 128600 | /* ePragTyp: */ PragTyp_INDEX_INFO, |
| 128589 | 128601 | /* ePragFlg: */ PragFlg_NeedSchema|PragFlg_Result1|PragFlg_SchemaOpt, |
| 128590 | - /* ColNames: */ 15, 6, | |
| 128602 | + /* ColNames: */ 21, 6, | |
| 128591 | 128603 | /* iArg: */ 1 }, |
| 128592 | 128604 | #endif |
| 128593 | 128605 | #if !defined(SQLITE_OMIT_INTEGRITY_CHECK) |
| 128594 | 128606 | {/* zName: */ "integrity_check", |
| 128595 | 128607 | /* ePragTyp: */ PragTyp_INTEGRITY_CHECK, |
| @@ -128625,11 +128637,11 @@ | ||
| 128625 | 128637 | #endif |
| 128626 | 128638 | #if defined(SQLITE_DEBUG) || defined(SQLITE_TEST) |
| 128627 | 128639 | {/* zName: */ "lock_status", |
| 128628 | 128640 | /* ePragTyp: */ PragTyp_LOCK_STATUS, |
| 128629 | 128641 | /* ePragFlg: */ PragFlg_Result0, |
| 128630 | - /* ColNames: */ 47, 2, | |
| 128642 | + /* ColNames: */ 53, 2, | |
| 128631 | 128643 | /* iArg: */ 0 }, |
| 128632 | 128644 | #endif |
| 128633 | 128645 | #if !defined(SQLITE_OMIT_PAGER_PRAGMAS) |
| 128634 | 128646 | {/* zName: */ "locking_mode", |
| 128635 | 128647 | /* ePragTyp: */ PragTyp_LOCKING_MODE, |
| @@ -128764,11 +128776,11 @@ | ||
| 128764 | 128776 | #endif |
| 128765 | 128777 | #if !defined(SQLITE_OMIT_SCHEMA_PRAGMAS) && defined(SQLITE_DEBUG) |
| 128766 | 128778 | {/* zName: */ "stats", |
| 128767 | 128779 | /* ePragTyp: */ PragTyp_STATS, |
| 128768 | 128780 | /* ePragFlg: */ PragFlg_NeedSchema|PragFlg_Result0|PragFlg_SchemaReq, |
| 128769 | - /* ColNames: */ 27, 5, | |
| 128781 | + /* ColNames: */ 33, 5, | |
| 128770 | 128782 | /* iArg: */ 0 }, |
| 128771 | 128783 | #endif |
| 128772 | 128784 | #if !defined(SQLITE_OMIT_PAGER_PRAGMAS) |
| 128773 | 128785 | {/* zName: */ "synchronous", |
| 128774 | 128786 | /* ePragTyp: */ PragTyp_SYNCHRONOUS, |
| @@ -128780,10 +128792,15 @@ | ||
| 128780 | 128792 | {/* zName: */ "table_info", |
| 128781 | 128793 | /* ePragTyp: */ PragTyp_TABLE_INFO, |
| 128782 | 128794 | /* ePragFlg: */ PragFlg_NeedSchema|PragFlg_Result1|PragFlg_SchemaOpt, |
| 128783 | 128795 | /* ColNames: */ 8, 6, |
| 128784 | 128796 | /* iArg: */ 0 }, |
| 128797 | + {/* zName: */ "table_list", | |
| 128798 | + /* ePragTyp: */ PragTyp_TABLE_LIST, | |
| 128799 | + /* ePragFlg: */ PragFlg_NeedSchema|PragFlg_Result1, | |
| 128800 | + /* ColNames: */ 15, 6, | |
| 128801 | + /* iArg: */ 1 }, | |
| 128785 | 128802 | {/* zName: */ "table_xinfo", |
| 128786 | 128803 | /* ePragTyp: */ PragTyp_TABLE_INFO, |
| 128787 | 128804 | /* ePragFlg: */ PragFlg_NeedSchema|PragFlg_Result1|PragFlg_SchemaOpt, |
| 128788 | 128805 | /* ColNames: */ 8, 7, |
| 128789 | 128806 | /* iArg: */ 1 }, |
| @@ -128855,11 +128872,11 @@ | ||
| 128855 | 128872 | /* ColNames: */ 0, 0, |
| 128856 | 128873 | /* iArg: */ 0 }, |
| 128857 | 128874 | {/* zName: */ "wal_checkpoint", |
| 128858 | 128875 | /* ePragTyp: */ PragTyp_WAL_CHECKPOINT, |
| 128859 | 128876 | /* ePragFlg: */ PragFlg_NeedSchema, |
| 128860 | - /* ColNames: */ 44, 3, | |
| 128877 | + /* ColNames: */ 50, 3, | |
| 128861 | 128878 | /* iArg: */ 0 }, |
| 128862 | 128879 | #endif |
| 128863 | 128880 | #if !defined(SQLITE_OMIT_FLAG_PRAGMAS) |
| 128864 | 128881 | {/* zName: */ "writable_schema", |
| 128865 | 128882 | /* ePragTyp: */ PragTyp_FLAG, |
| @@ -128866,11 +128883,11 @@ | ||
| 128866 | 128883 | /* ePragFlg: */ PragFlg_Result0|PragFlg_NoColumns1, |
| 128867 | 128884 | /* ColNames: */ 0, 0, |
| 128868 | 128885 | /* iArg: */ SQLITE_WriteSchema|SQLITE_NoSchemaError }, |
| 128869 | 128886 | #endif |
| 128870 | 128887 | }; |
| 128871 | -/* Number of pragmas: 67 on by default, 77 total. */ | |
| 128888 | +/* Number of pragmas: 68 on by default, 78 total. */ | |
| 128872 | 128889 | |
| 128873 | 128890 | /************** End of pragma.h **********************************************/ |
| 128874 | 128891 | /************** Continuing where we left off in pragma.c *********************/ |
| 128875 | 128892 | |
| 128876 | 128893 | /* |
| @@ -130035,10 +130052,58 @@ | ||
| 130035 | 130052 | } |
| 130036 | 130053 | } |
| 130037 | 130054 | } |
| 130038 | 130055 | break; |
| 130039 | 130056 | |
| 130057 | + /* | |
| 130058 | + ** PRAGMA table_list | |
| 130059 | + ** | |
| 130060 | + ** Return a single row for each table, virtual table, or view in the | |
| 130061 | + ** entire schema. | |
| 130062 | + ** | |
| 130063 | + ** schema: Name of attached database hold this table | |
| 130064 | + ** name: Name of the table itself | |
| 130065 | + ** type: "table", "view", "virtual", "shadow" | |
| 130066 | + ** ncol: Number of columns | |
| 130067 | + ** wr: True for a WITHOUT ROWID table | |
| 130068 | + ** strict: True for a STRICT table | |
| 130069 | + */ | |
| 130070 | + case PragTyp_TABLE_LIST: { | |
| 130071 | + int ii; | |
| 130072 | + pParse->nMem = 6; | |
| 130073 | + sqlite3CodeVerifyNamedSchema(pParse, zDb); | |
| 130074 | + for(ii=0; ii<db->nDb; ii++){ | |
| 130075 | + HashElem *k; | |
| 130076 | + Hash *pHash; | |
| 130077 | + if( zDb && sqlite3_stricmp(zDb, db->aDb[ii].zDbSName)!=0 ) continue; | |
| 130078 | + pHash = &db->aDb[ii].pSchema->tblHash; | |
| 130079 | + for(k=sqliteHashFirst(pHash); k; k=sqliteHashNext(k) ){ | |
| 130080 | + Table *pTab = sqliteHashData(k); | |
| 130081 | + const char *zType; | |
| 130082 | + if( zRight && sqlite3_stricmp(zRight, pTab->zName)!=0 ) continue; | |
| 130083 | + if( IsView(pTab) ){ | |
| 130084 | + zType = "view"; | |
| 130085 | + }else if( IsVirtual(pTab) ){ | |
| 130086 | + zType = "virtual"; | |
| 130087 | + }else if( pTab->tabFlags & TF_Shadow ){ | |
| 130088 | + zType = "shadow"; | |
| 130089 | + }else{ | |
| 130090 | + zType = "table"; | |
| 130091 | + } | |
| 130092 | + sqlite3VdbeMultiLoad(v, 1, "sssiii", | |
| 130093 | + db->aDb[ii].zDbSName, | |
| 130094 | + pTab->zName, | |
| 130095 | + zType, | |
| 130096 | + pTab->nCol, | |
| 130097 | + (pTab->tabFlags & TF_WithoutRowid)!=0, | |
| 130098 | + (pTab->tabFlags & TF_Strict)!=0 | |
| 130099 | + ); | |
| 130100 | + } | |
| 130101 | + } | |
| 130102 | + } | |
| 130103 | + break; | |
| 130104 | + | |
| 130040 | 130105 | #ifdef SQLITE_DEBUG |
| 130041 | 130106 | case PragTyp_STATS: { |
| 130042 | 130107 | Index *pIdx; |
| 130043 | 130108 | HashElem *i; |
| 130044 | 130109 | pParse->nMem = 5; |
| @@ -130544,11 +130609,13 @@ | ||
| 130544 | 130609 | }else{ |
| 130545 | 130610 | integrityCheckResultRow(v); |
| 130546 | 130611 | } |
| 130547 | 130612 | sqlite3VdbeJumpHere(v, jmp2); |
| 130548 | 130613 | } |
| 130549 | - if( pTab->tabFlags & TF_Strict ){ | |
| 130614 | + if( (pTab->tabFlags & TF_Strict)!=0 | |
| 130615 | + && pCol->eCType!=COLTYPE_ANY | |
| 130616 | + ){ | |
| 130550 | 130617 | jmp2 = sqlite3VdbeAddOp3(v, OP_IsNullOrType, 3, 0, |
| 130551 | 130618 | sqlite3StdTypeMap[pCol->eCType-1]); |
| 130552 | 130619 | VdbeCoverage(v); |
| 130553 | 130620 | zErr = sqlite3MPrintf(db, "non-%s value in %s.%s", |
| 130554 | 130621 | sqlite3StdType[pCol->eCType-1], |
| @@ -132836,10 +132903,13 @@ | ||
| 132836 | 132903 | |
| 132837 | 132904 | pE1 = sqlite3CreateColumnExpr(db, pSrc, iLeft, iColLeft); |
| 132838 | 132905 | pE2 = sqlite3CreateColumnExpr(db, pSrc, iRight, iColRight); |
| 132839 | 132906 | |
| 132840 | 132907 | pEq = sqlite3PExpr(pParse, TK_EQ, pE1, pE2); |
| 132908 | + assert( pE2!=0 || pEq==0 ); /* Due to db->mallocFailed test | |
| 132909 | + ** in sqlite3DbMallocRawNN() called from | |
| 132910 | + ** sqlite3PExpr(). */ | |
| 132841 | 132911 | if( pEq && isOuterJoin ){ |
| 132842 | 132912 | ExprSetProperty(pEq, EP_FromJoin); |
| 132843 | 132913 | assert( !ExprHasProperty(pEq, EP_TokenOnly|EP_Reduced) ); |
| 132844 | 132914 | ExprSetVVAProperty(pEq, EP_NoReduce); |
| 132845 | 132915 | pEq->iRightJoinTable = pE2->iTable; |
| @@ -148365,11 +148435,11 @@ | ||
| 148365 | 148435 | ** 2019-06-14 https://sqlite.org/src/info/ce8717f0885af975 |
| 148366 | 148436 | ** 2019-09-03 https://sqlite.org/src/info/0f0428096f17252a |
| 148367 | 148437 | */ |
| 148368 | 148438 | if( pLeft->op!=TK_COLUMN |
| 148369 | 148439 | || sqlite3ExprAffinity(pLeft)!=SQLITE_AFF_TEXT |
| 148370 | - || IsVirtual(pLeft->y.pTab) /* Value might be numeric */ | |
| 148440 | + || (pLeft->y.pTab && IsVirtual(pLeft->y.pTab)) /* Might be numeric */ | |
| 148371 | 148441 | ){ |
| 148372 | 148442 | int isNum; |
| 148373 | 148443 | double rDummy; |
| 148374 | 148444 | isNum = sqlite3AtoF(zNew, &rDummy, iTo, SQLITE_UTF8); |
| 148375 | 148445 | if( isNum<=0 ){ |
| @@ -156490,10 +156560,13 @@ | ||
| 156490 | 156560 | ); |
| 156491 | 156561 | SELECTTRACE(1,pParse,pSub, |
| 156492 | 156562 | ("New window-function subquery in FROM clause of (%u/%p)\n", |
| 156493 | 156563 | p->selId, p)); |
| 156494 | 156564 | p->pSrc = sqlite3SrcListAppend(pParse, 0, 0, 0); |
| 156565 | + assert( pSub!=0 || p->pSrc==0 ); /* Due to db->mallocFailed test inside | |
| 156566 | + ** of sqlite3DbMallocRawNN() called from | |
| 156567 | + ** sqlite3SrcListAppend() */ | |
| 156495 | 156568 | if( p->pSrc ){ |
| 156496 | 156569 | Table *pTab2; |
| 156497 | 156570 | p->pSrc->a[0].pSelect = pSub; |
| 156498 | 156571 | sqlite3SrcListAssignCursors(pParse, p->pSrc); |
| 156499 | 156572 | pSub->selFlags |= SF_Expanded|SF_OrderByReqd; |
| @@ -192908,11 +192981,15 @@ | ||
| 192908 | 192981 | #else |
| 192909 | 192982 | /* #include "sqlite3.h" */ |
| 192910 | 192983 | #endif |
| 192911 | 192984 | SQLITE_PRIVATE int sqlite3GetToken(const unsigned char*,int*); /* In the SQLite core */ |
| 192912 | 192985 | |
| 192913 | -#ifndef SQLITE_AMALGAMATION | |
| 192986 | +/* | |
| 192987 | +** If building separately, we will need some setup that is normally | |
| 192988 | +** found in sqliteInt.h | |
| 192989 | +*/ | |
| 192990 | +#if !defined(SQLITE_AMALGAMATION) | |
| 192914 | 192991 | #include "sqlite3rtree.h" |
| 192915 | 192992 | typedef sqlite3_int64 i64; |
| 192916 | 192993 | typedef sqlite3_uint64 u64; |
| 192917 | 192994 | typedef unsigned char u8; |
| 192918 | 192995 | typedef unsigned short u16; |
| @@ -192921,11 +192998,21 @@ | ||
| 192921 | 192998 | # define NDEBUG 1 |
| 192922 | 192999 | #endif |
| 192923 | 193000 | #if defined(NDEBUG) && defined(SQLITE_DEBUG) |
| 192924 | 193001 | # undef NDEBUG |
| 192925 | 193002 | #endif |
| 193003 | +#if defined(SQLITE_COVERAGE_TEST) || defined(SQLITE_MUTATION_TEST) | |
| 193004 | +# define ALWAYS(X) (1) | |
| 193005 | +# define NEVER(X) (0) | |
| 193006 | +#elif !defined(NDEBUG) | |
| 193007 | +# define ALWAYS(X) ((X)?1:(assert(0),0)) | |
| 193008 | +# define NEVER(X) ((X)?(assert(0),1):0) | |
| 193009 | +#else | |
| 193010 | +# define ALWAYS(X) (X) | |
| 193011 | +# define NEVER(X) (X) | |
| 192926 | 193012 | #endif |
| 193013 | +#endif /* !defined(SQLITE_AMALGAMATION) */ | |
| 192927 | 193014 | |
| 192928 | 193015 | /* #include <string.h> */ |
| 192929 | 193016 | /* #include <stdio.h> */ |
| 192930 | 193017 | /* #include <assert.h> */ |
| 192931 | 193018 | /* #include <stdlib.h> */ |
| @@ -192979,11 +193066,13 @@ | ||
| 192979 | 193066 | u8 nDim2; /* Twice the number of dimensions */ |
| 192980 | 193067 | u8 eCoordType; /* RTREE_COORD_REAL32 or RTREE_COORD_INT32 */ |
| 192981 | 193068 | u8 nBytesPerCell; /* Bytes consumed per cell */ |
| 192982 | 193069 | u8 inWrTrans; /* True if inside write transaction */ |
| 192983 | 193070 | u8 nAux; /* # of auxiliary columns in %_rowid */ |
| 193071 | +#ifdef SQLITE_ENABLE_GEOPOLY | |
| 192984 | 193072 | u8 nAuxNotNull; /* Number of initial not-null aux columns */ |
| 193073 | +#endif | |
| 192985 | 193074 | #ifdef SQLITE_DEBUG |
| 192986 | 193075 | u8 bCorrupt; /* Shadow table corruption detected */ |
| 192987 | 193076 | #endif |
| 192988 | 193077 | int iDepth; /* Current depth of the r-tree structure */ |
| 192989 | 193078 | char *zDb; /* Name of database containing r-tree table */ |
| @@ -193510,22 +193599,10 @@ | ||
| 193510 | 193599 | pRtree->pNodeBlob = 0; |
| 193511 | 193600 | sqlite3_blob_close(pBlob); |
| 193512 | 193601 | } |
| 193513 | 193602 | } |
| 193514 | 193603 | |
| 193515 | -/* | |
| 193516 | -** Check to see if pNode is the same as pParent or any of the parents | |
| 193517 | -** of pParent. | |
| 193518 | -*/ | |
| 193519 | -static int nodeInParentChain(const RtreeNode *pNode, const RtreeNode *pParent){ | |
| 193520 | - do{ | |
| 193521 | - if( pNode==pParent ) return 1; | |
| 193522 | - pParent = pParent->pParent; | |
| 193523 | - }while( pParent ); | |
| 193524 | - return 0; | |
| 193525 | -} | |
| 193526 | - | |
| 193527 | 193604 | /* |
| 193528 | 193605 | ** Obtain a reference to an r-tree node. |
| 193529 | 193606 | */ |
| 193530 | 193607 | static int nodeAcquire( |
| 193531 | 193608 | Rtree *pRtree, /* R-tree structure */ |
| @@ -193538,18 +193615,11 @@ | ||
| 193538 | 193615 | |
| 193539 | 193616 | /* Check if the requested node is already in the hash table. If so, |
| 193540 | 193617 | ** increase its reference count and return it. |
| 193541 | 193618 | */ |
| 193542 | 193619 | if( (pNode = nodeHashLookup(pRtree, iNode))!=0 ){ |
| 193543 | - if( pParent && !pNode->pParent ){ | |
| 193544 | - if( nodeInParentChain(pNode, pParent) ){ | |
| 193545 | - RTREE_IS_CORRUPT(pRtree); | |
| 193546 | - return SQLITE_CORRUPT_VTAB; | |
| 193547 | - } | |
| 193548 | - pParent->nRef++; | |
| 193549 | - pNode->pParent = pParent; | |
| 193550 | - }else if( pParent && pNode->pParent && pParent!=pNode->pParent ){ | |
| 193620 | + if( pParent && pParent!=pNode->pParent ){ | |
| 193551 | 193621 | RTREE_IS_CORRUPT(pRtree); |
| 193552 | 193622 | return SQLITE_CORRUPT_VTAB; |
| 193553 | 193623 | } |
| 193554 | 193624 | pNode->nRef++; |
| 193555 | 193625 | *ppNode = pNode; |
| @@ -193603,11 +193673,11 @@ | ||
| 193603 | 193673 | ** of the r-tree structure. A height of zero means all data is stored on |
| 193604 | 193674 | ** the root node. A height of one means the children of the root node |
| 193605 | 193675 | ** are the leaves, and so on. If the depth as specified on the root node |
| 193606 | 193676 | ** is greater than RTREE_MAX_DEPTH, the r-tree structure must be corrupt. |
| 193607 | 193677 | */ |
| 193608 | - if( pNode && rc==SQLITE_OK && iNode==1 ){ | |
| 193678 | + if( rc==SQLITE_OK && pNode && iNode==1 ){ | |
| 193609 | 193679 | pRtree->iDepth = readInt16(pNode->zData); |
| 193610 | 193680 | if( pRtree->iDepth>RTREE_MAX_DEPTH ){ |
| 193611 | 193681 | rc = SQLITE_CORRUPT_VTAB; |
| 193612 | 193682 | RTREE_IS_CORRUPT(pRtree); |
| 193613 | 193683 | } |
| @@ -194209,15 +194279,16 @@ | ||
| 194209 | 194279 | ** Return the index of the cell containing a pointer to node pNode |
| 194210 | 194280 | ** in its parent. If pNode is the root node, return -1. |
| 194211 | 194281 | */ |
| 194212 | 194282 | static int nodeParentIndex(Rtree *pRtree, RtreeNode *pNode, int *piIndex){ |
| 194213 | 194283 | RtreeNode *pParent = pNode->pParent; |
| 194214 | - if( pParent ){ | |
| 194284 | + if( ALWAYS(pParent) ){ | |
| 194215 | 194285 | return nodeRowidIndex(pRtree, pParent, pNode->iNode, piIndex); |
| 194286 | + }else{ | |
| 194287 | + *piIndex = -1; | |
| 194288 | + return SQLITE_OK; | |
| 194216 | 194289 | } |
| 194217 | - *piIndex = -1; | |
| 194218 | - return SQLITE_OK; | |
| 194219 | 194290 | } |
| 194220 | 194291 | |
| 194221 | 194292 | /* |
| 194222 | 194293 | ** Compare two search points. Return negative, zero, or positive if the first |
| 194223 | 194294 | ** is less than, equal to, or greater than the second. |
| @@ -194336,11 +194407,12 @@ | ||
| 194336 | 194407 | if( pCur->bPoint ){ |
| 194337 | 194408 | int ii; |
| 194338 | 194409 | pNew = rtreeEnqueue(pCur, rScore, iLevel); |
| 194339 | 194410 | if( pNew==0 ) return 0; |
| 194340 | 194411 | ii = (int)(pNew - pCur->aPoint) + 1; |
| 194341 | - if( ii<RTREE_CACHE_SZ ){ | |
| 194412 | + assert( ii==1 ); | |
| 194413 | + if( ALWAYS(ii<RTREE_CACHE_SZ) ){ | |
| 194342 | 194414 | assert( pCur->aNode[ii]==0 ); |
| 194343 | 194415 | pCur->aNode[ii] = pCur->aNode[0]; |
| 194344 | 194416 | }else{ |
| 194345 | 194417 | nodeRelease(RTREE_OF_CURSOR(pCur), pCur->aNode[0]); |
| 194346 | 194418 | } |
| @@ -194397,11 +194469,11 @@ | ||
| 194397 | 194469 | p->aNode[i] = 0; |
| 194398 | 194470 | } |
| 194399 | 194471 | if( p->bPoint ){ |
| 194400 | 194472 | p->anQueue[p->sPoint.iLevel]--; |
| 194401 | 194473 | p->bPoint = 0; |
| 194402 | - }else if( p->nPoint ){ | |
| 194474 | + }else if( ALWAYS(p->nPoint) ){ | |
| 194403 | 194475 | p->anQueue[p->aPoint[0].iLevel]--; |
| 194404 | 194476 | n = --p->nPoint; |
| 194405 | 194477 | p->aPoint[0] = p->aPoint[n]; |
| 194406 | 194478 | if( n<RTREE_CACHE_SZ-1 ){ |
| 194407 | 194479 | p->aNode[1] = p->aNode[n+1]; |
| @@ -194538,11 +194610,11 @@ | ||
| 194538 | 194610 | static int rtreeRowid(sqlite3_vtab_cursor *pVtabCursor, sqlite_int64 *pRowid){ |
| 194539 | 194611 | RtreeCursor *pCsr = (RtreeCursor *)pVtabCursor; |
| 194540 | 194612 | RtreeSearchPoint *p = rtreeSearchPointFirst(pCsr); |
| 194541 | 194613 | int rc = SQLITE_OK; |
| 194542 | 194614 | RtreeNode *pNode = rtreeNodeOfFirstSearchPoint(pCsr, &rc); |
| 194543 | - if( rc==SQLITE_OK && p ){ | |
| 194615 | + if( rc==SQLITE_OK && ALWAYS(p) ){ | |
| 194544 | 194616 | *pRowid = nodeGetRowid(RTREE_OF_CURSOR(pCsr), pNode, p->iCell); |
| 194545 | 194617 | } |
| 194546 | 194618 | return rc; |
| 194547 | 194619 | } |
| 194548 | 194620 | |
| @@ -194556,11 +194628,11 @@ | ||
| 194556 | 194628 | RtreeCoord c; |
| 194557 | 194629 | int rc = SQLITE_OK; |
| 194558 | 194630 | RtreeNode *pNode = rtreeNodeOfFirstSearchPoint(pCsr, &rc); |
| 194559 | 194631 | |
| 194560 | 194632 | if( rc ) return rc; |
| 194561 | - if( p==0 ) return SQLITE_OK; | |
| 194633 | + if( NEVER(p==0) ) return SQLITE_OK; | |
| 194562 | 194634 | if( i==0 ){ |
| 194563 | 194635 | sqlite3_result_int64(ctx, nodeGetRowid(pRtree, pNode, p->iCell)); |
| 194564 | 194636 | }else if( i<=pRtree->nDim2 ){ |
| 194565 | 194637 | nodeGetCoord(pRtree, pNode, p->iCell, i-1, &c); |
| 194566 | 194638 | #ifndef SQLITE_RTREE_INT_ONLY |
| @@ -194755,12 +194827,15 @@ | ||
| 194755 | 194827 | } |
| 194756 | 194828 | } |
| 194757 | 194829 | } |
| 194758 | 194830 | if( rc==SQLITE_OK ){ |
| 194759 | 194831 | RtreeSearchPoint *pNew; |
| 194832 | + assert( pCsr->bPoint==0 ); /* Due to the resetCursor() call above */ | |
| 194760 | 194833 | pNew = rtreeSearchPointNew(pCsr, RTREE_ZERO, (u8)(pRtree->iDepth+1)); |
| 194761 | - if( pNew==0 ) return SQLITE_NOMEM; | |
| 194834 | + if( NEVER(pNew==0) ){ /* Because pCsr->bPoint was FALSE */ | |
| 194835 | + return SQLITE_NOMEM; | |
| 194836 | + } | |
| 194762 | 194837 | pNew->id = 1; |
| 194763 | 194838 | pNew->iCell = 0; |
| 194764 | 194839 | pNew->eWithin = PARTLY_WITHIN; |
| 194765 | 194840 | assert( pCsr->bPoint==1 ); |
| 194766 | 194841 | pCsr->aNode[0] = pRoot; |
| @@ -194833,11 +194908,11 @@ | ||
| 194833 | 194908 | assert( pIdxInfo->idxStr==0 ); |
| 194834 | 194909 | for(ii=0; ii<pIdxInfo->nConstraint && iIdx<(int)(sizeof(zIdxStr)-1); ii++){ |
| 194835 | 194910 | struct sqlite3_index_constraint *p = &pIdxInfo->aConstraint[ii]; |
| 194836 | 194911 | |
| 194837 | 194912 | if( bMatch==0 && p->usable |
| 194838 | - && p->iColumn==0 && p->op==SQLITE_INDEX_CONSTRAINT_EQ | |
| 194913 | + && p->iColumn<=0 && p->op==SQLITE_INDEX_CONSTRAINT_EQ | |
| 194839 | 194914 | ){ |
| 194840 | 194915 | /* We have an equality constraint on the rowid. Use strategy 1. */ |
| 194841 | 194916 | int jj; |
| 194842 | 194917 | for(jj=0; jj<ii; jj++){ |
| 194843 | 194918 | pIdxInfo->aConstraintUsage[jj].argvIndex = 0; |
| @@ -195086,16 +195161,23 @@ | ||
| 195086 | 195161 | RtreeNode *pNode, /* Adjust ancestry of this node. */ |
| 195087 | 195162 | RtreeCell *pCell /* This cell was just inserted */ |
| 195088 | 195163 | ){ |
| 195089 | 195164 | RtreeNode *p = pNode; |
| 195090 | 195165 | int cnt = 0; |
| 195166 | + int rc; | |
| 195091 | 195167 | while( p->pParent ){ |
| 195092 | 195168 | RtreeNode *pParent = p->pParent; |
| 195093 | 195169 | RtreeCell cell; |
| 195094 | 195170 | int iCell; |
| 195095 | 195171 | |
| 195096 | - if( (++cnt)>1000 || nodeParentIndex(pRtree, p, &iCell) ){ | |
| 195172 | + cnt++; | |
| 195173 | + if( NEVER(cnt>100) ){ | |
| 195174 | + RTREE_IS_CORRUPT(pRtree); | |
| 195175 | + return SQLITE_CORRUPT_VTAB; | |
| 195176 | + } | |
| 195177 | + rc = nodeParentIndex(pRtree, p, &iCell); | |
| 195178 | + if( NEVER(rc!=SQLITE_OK) ){ | |
| 195097 | 195179 | RTREE_IS_CORRUPT(pRtree); |
| 195098 | 195180 | return SQLITE_CORRUPT_VTAB; |
| 195099 | 195181 | } |
| 195100 | 195182 | |
| 195101 | 195183 | nodeGetCell(pRtree, pParent, iCell, &cell); |
| @@ -195475,15 +195557,16 @@ | ||
| 195475 | 195557 | } |
| 195476 | 195558 | }else{ |
| 195477 | 195559 | RtreeNode *pParent = pLeft->pParent; |
| 195478 | 195560 | int iCell; |
| 195479 | 195561 | rc = nodeParentIndex(pRtree, pLeft, &iCell); |
| 195480 | - if( rc==SQLITE_OK ){ | |
| 195562 | + if( ALWAYS(rc==SQLITE_OK) ){ | |
| 195481 | 195563 | nodeOverwriteCell(pRtree, pParent, &leftbbox, iCell); |
| 195482 | 195564 | rc = AdjustTree(pRtree, pParent, &leftbbox); |
| 195565 | + assert( rc==SQLITE_OK ); | |
| 195483 | 195566 | } |
| 195484 | - if( rc!=SQLITE_OK ){ | |
| 195567 | + if( NEVER(rc!=SQLITE_OK) ){ | |
| 195485 | 195568 | goto splitnode_out; |
| 195486 | 195569 | } |
| 195487 | 195570 | } |
| 195488 | 195571 | if( (rc = rtreeInsertCell(pRtree, pRight->pParent, &rightbbox, iHeight+1)) ){ |
| 195489 | 195572 | goto splitnode_out; |
| @@ -195554,11 +195637,11 @@ | ||
| 195554 | 195637 | ** want to do this as it leads to a memory leak when trying to delete |
| 195555 | 195638 | ** the referenced counted node structures. |
| 195556 | 195639 | */ |
| 195557 | 195640 | iNode = sqlite3_column_int64(pRtree->pReadParent, 0); |
| 195558 | 195641 | for(pTest=pLeaf; pTest && pTest->iNode!=iNode; pTest=pTest->pParent); |
| 195559 | - if( !pTest ){ | |
| 195642 | + if( pTest==0 ){ | |
| 195560 | 195643 | rc2 = nodeAcquire(pRtree, iNode, 0, &pChild->pParent); |
| 195561 | 195644 | } |
| 195562 | 195645 | } |
| 195563 | 195646 | rc = sqlite3_reset(pRtree->pReadParent); |
| 195564 | 195647 | if( rc==SQLITE_OK ) rc = rc2; |
| @@ -195585,10 +195668,11 @@ | ||
| 195585 | 195668 | rc = nodeParentIndex(pRtree, pNode, &iCell); |
| 195586 | 195669 | if( rc==SQLITE_OK ){ |
| 195587 | 195670 | pParent = pNode->pParent; |
| 195588 | 195671 | pNode->pParent = 0; |
| 195589 | 195672 | rc = deleteCell(pRtree, pParent, iCell, iHeight+1); |
| 195673 | + testcase( rc!=SQLITE_OK ); | |
| 195590 | 195674 | } |
| 195591 | 195675 | rc2 = nodeRelease(pRtree, pParent); |
| 195592 | 195676 | if( rc==SQLITE_OK ){ |
| 195593 | 195677 | rc = rc2; |
| 195594 | 195678 | } |
| @@ -195807,11 +195891,11 @@ | ||
| 195807 | 195891 | pRtree->iReinsertHeight = iHeight; |
| 195808 | 195892 | rc = Reinsert(pRtree, pNode, pCell, iHeight); |
| 195809 | 195893 | } |
| 195810 | 195894 | }else{ |
| 195811 | 195895 | rc = AdjustTree(pRtree, pNode, pCell); |
| 195812 | - if( rc==SQLITE_OK ){ | |
| 195896 | + if( ALWAYS(rc==SQLITE_OK) ){ | |
| 195813 | 195897 | if( iHeight==0 ){ |
| 195814 | 195898 | rc = rowidWrite(pRtree, pCell->iRowid, pNode->iNode); |
| 195815 | 195899 | }else{ |
| 195816 | 195900 | rc = parentWrite(pRtree, pCell->iRowid, pNode->iNode); |
| 195817 | 195901 | } |
| @@ -195913,11 +195997,11 @@ | ||
| 195913 | 195997 | */ |
| 195914 | 195998 | if( rc==SQLITE_OK && pRtree->iDepth>0 && NCELL(pRoot)==1 ){ |
| 195915 | 195999 | int rc2; |
| 195916 | 196000 | RtreeNode *pChild = 0; |
| 195917 | 196001 | i64 iChild = nodeGetRowid(pRtree, pRoot, 0); |
| 195918 | - rc = nodeAcquire(pRtree, iChild, pRoot, &pChild); | |
| 196002 | + rc = nodeAcquire(pRtree, iChild, pRoot, &pChild); /* tag-20210916a */ | |
| 195919 | 196003 | if( rc==SQLITE_OK ){ |
| 195920 | 196004 | rc = removeNode(pRtree, pChild, pRtree->iDepth-1); |
| 195921 | 196005 | } |
| 195922 | 196006 | rc2 = nodeRelease(pRtree, pChild); |
| 195923 | 196007 | if( rc==SQLITE_OK ) rc = rc2; |
| @@ -196248,11 +196332,11 @@ | ||
| 196248 | 196332 | static int rtreeQueryStat1(sqlite3 *db, Rtree *pRtree){ |
| 196249 | 196333 | const char *zFmt = "SELECT stat FROM %Q.sqlite_stat1 WHERE tbl = '%q_rowid'"; |
| 196250 | 196334 | char *zSql; |
| 196251 | 196335 | sqlite3_stmt *p; |
| 196252 | 196336 | int rc; |
| 196253 | - i64 nRow = 0; | |
| 196337 | + i64 nRow = RTREE_MIN_ROWEST; | |
| 196254 | 196338 | |
| 196255 | 196339 | rc = sqlite3_table_column_metadata( |
| 196256 | 196340 | db, pRtree->zDb, "sqlite_stat1",0,0,0,0,0,0 |
| 196257 | 196341 | ); |
| 196258 | 196342 | if( rc!=SQLITE_OK ){ |
| @@ -196265,24 +196349,14 @@ | ||
| 196265 | 196349 | }else{ |
| 196266 | 196350 | rc = sqlite3_prepare_v2(db, zSql, -1, &p, 0); |
| 196267 | 196351 | if( rc==SQLITE_OK ){ |
| 196268 | 196352 | if( sqlite3_step(p)==SQLITE_ROW ) nRow = sqlite3_column_int64(p, 0); |
| 196269 | 196353 | rc = sqlite3_finalize(p); |
| 196270 | - }else if( rc!=SQLITE_NOMEM ){ | |
| 196271 | - rc = SQLITE_OK; | |
| 196272 | - } | |
| 196273 | - | |
| 196274 | - if( rc==SQLITE_OK ){ | |
| 196275 | - if( nRow==0 ){ | |
| 196276 | - pRtree->nRowEst = RTREE_DEFAULT_ROWEST; | |
| 196277 | - }else{ | |
| 196278 | - pRtree->nRowEst = MAX(nRow, RTREE_MIN_ROWEST); | |
| 196279 | - } | |
| 196280 | 196354 | } |
| 196281 | 196355 | sqlite3_free(zSql); |
| 196282 | 196356 | } |
| 196283 | - | |
| 196357 | + pRtree->nRowEst = MAX(nRow, RTREE_MIN_ROWEST); | |
| 196284 | 196358 | return rc; |
| 196285 | 196359 | } |
| 196286 | 196360 | |
| 196287 | 196361 | |
| 196288 | 196362 | /* |
| @@ -196428,13 +196502,16 @@ | ||
| 196428 | 196502 | int ii; |
| 196429 | 196503 | char *zSql; |
| 196430 | 196504 | sqlite3_str_appendf(p, "UPDATE \"%w\".\"%w_rowid\"SET ", zDb, zPrefix); |
| 196431 | 196505 | for(ii=0; ii<pRtree->nAux; ii++){ |
| 196432 | 196506 | if( ii ) sqlite3_str_append(p, ",", 1); |
| 196507 | +#ifdef SQLITE_ENABLE_GEOPOLY | |
| 196433 | 196508 | if( ii<pRtree->nAuxNotNull ){ |
| 196434 | 196509 | sqlite3_str_appendf(p,"a%d=coalesce(?%d,a%d)",ii,ii+2,ii); |
| 196435 | - }else{ | |
| 196510 | + }else | |
| 196511 | +#endif | |
| 196512 | + { | |
| 196436 | 196513 | sqlite3_str_appendf(p,"a%d=?%d",ii,ii+2); |
| 196437 | 196514 | } |
| 196438 | 196515 | } |
| 196439 | 196516 | sqlite3_str_appendf(p, " WHERE rowid=?1"); |
| 196440 | 196517 | zSql = sqlite3_str_finish(p); |
| @@ -197109,12 +197186,14 @@ | ||
| 197109 | 197186 | if( check.rc==SQLITE_OK ){ |
| 197110 | 197187 | pStmt = rtreeCheckPrepare(&check, "SELECT * FROM %Q.'%q_rowid'", zDb, zTab); |
| 197111 | 197188 | if( pStmt ){ |
| 197112 | 197189 | nAux = sqlite3_column_count(pStmt) - 2; |
| 197113 | 197190 | sqlite3_finalize(pStmt); |
| 197191 | + }else | |
| 197192 | + if( check.rc!=SQLITE_NOMEM ){ | |
| 197193 | + check.rc = SQLITE_OK; | |
| 197114 | 197194 | } |
| 197115 | - check.rc = SQLITE_OK; | |
| 197116 | 197195 | } |
| 197117 | 197196 | |
| 197118 | 197197 | /* Find number of dimensions in the rtree table. */ |
| 197119 | 197198 | pStmt = rtreeCheckPrepare(&check, "SELECT * FROM %Q.%Q", zDb, zTab); |
| 197120 | 197199 | if( pStmt ){ |
| @@ -199185,11 +199264,14 @@ | ||
| 199185 | 199264 | ){ |
| 199186 | 199265 | RtreeGeomCallback *pGeomCtx; /* Context object for new user-function */ |
| 199187 | 199266 | |
| 199188 | 199267 | /* Allocate and populate the context object. */ |
| 199189 | 199268 | pGeomCtx = (RtreeGeomCallback *)sqlite3_malloc(sizeof(RtreeGeomCallback)); |
| 199190 | - if( !pGeomCtx ) return SQLITE_NOMEM; | |
| 199269 | + if( !pGeomCtx ){ | |
| 199270 | + if( xDestructor ) xDestructor(pContext); | |
| 199271 | + return SQLITE_NOMEM; | |
| 199272 | + } | |
| 199191 | 199273 | pGeomCtx->xGeom = 0; |
| 199192 | 199274 | pGeomCtx->xQueryFunc = xQueryFunc; |
| 199193 | 199275 | pGeomCtx->xDestructor = xDestructor; |
| 199194 | 199276 | pGeomCtx->pContext = pContext; |
| 199195 | 199277 | return sqlite3_create_function_v2(db, zQueryFunc, -1, SQLITE_ANY, |
| @@ -222468,10 +222550,46 @@ | ||
| 222468 | 222550 | if( p->pStruct!=(Fts5Structure*)pStruct ){ |
| 222469 | 222551 | return SQLITE_ABORT; |
| 222470 | 222552 | } |
| 222471 | 222553 | return SQLITE_OK; |
| 222472 | 222554 | } |
| 222555 | + | |
| 222556 | +/* | |
| 222557 | +** Ensure that structure object (*pp) is writable. | |
| 222558 | +** | |
| 222559 | +** This function is a no-op if (*pRc) is not SQLITE_OK when it is called. If | |
| 222560 | +** an error occurs, (*pRc) is set to an SQLite error code before returning. | |
| 222561 | +*/ | |
| 222562 | +static void fts5StructureMakeWritable(int *pRc, Fts5Structure **pp){ | |
| 222563 | + Fts5Structure *p = *pp; | |
| 222564 | + if( *pRc==SQLITE_OK && p->nRef>1 ){ | |
| 222565 | + int nByte = sizeof(Fts5Structure)+(p->nLevel-1)*sizeof(Fts5StructureLevel); | |
| 222566 | + Fts5Structure *pNew; | |
| 222567 | + pNew = (Fts5Structure*)sqlite3Fts5MallocZero(pRc, nByte); | |
| 222568 | + if( pNew ){ | |
| 222569 | + int i; | |
| 222570 | + memcpy(pNew, p, nByte); | |
| 222571 | + for(i=0; i<p->nLevel; i++) pNew->aLevel[i].aSeg = 0; | |
| 222572 | + for(i=0; i<p->nLevel; i++){ | |
| 222573 | + Fts5StructureLevel *pLvl = &pNew->aLevel[i]; | |
| 222574 | + nByte = sizeof(Fts5StructureSegment) * pNew->aLevel[i].nSeg; | |
| 222575 | + pLvl->aSeg = (Fts5StructureSegment*)sqlite3Fts5MallocZero(pRc, nByte); | |
| 222576 | + if( pLvl->aSeg==0 ){ | |
| 222577 | + for(i=0; i<p->nLevel; i++){ | |
| 222578 | + sqlite3_free(pNew->aLevel[i].aSeg); | |
| 222579 | + } | |
| 222580 | + sqlite3_free(pNew); | |
| 222581 | + return; | |
| 222582 | + } | |
| 222583 | + memcpy(pLvl->aSeg, p->aLevel[i].aSeg, nByte); | |
| 222584 | + } | |
| 222585 | + p->nRef--; | |
| 222586 | + pNew->nRef = 1; | |
| 222587 | + } | |
| 222588 | + *pp = pNew; | |
| 222589 | + } | |
| 222590 | +} | |
| 222473 | 222591 | |
| 222474 | 222592 | /* |
| 222475 | 222593 | ** Deserialize and return the structure record currently stored in serialized |
| 222476 | 222594 | ** form within buffer pData/nData. |
| 222477 | 222595 | ** |
| @@ -222570,13 +222688,15 @@ | ||
| 222570 | 222688 | *ppOut = pRet; |
| 222571 | 222689 | return rc; |
| 222572 | 222690 | } |
| 222573 | 222691 | |
| 222574 | 222692 | /* |
| 222575 | -** | |
| 222693 | +** Add a level to the Fts5Structure.aLevel[] array of structure object | |
| 222694 | +** (*ppStruct). | |
| 222576 | 222695 | */ |
| 222577 | 222696 | static void fts5StructureAddLevel(int *pRc, Fts5Structure **ppStruct){ |
| 222697 | + fts5StructureMakeWritable(pRc, ppStruct); | |
| 222578 | 222698 | if( *pRc==SQLITE_OK ){ |
| 222579 | 222699 | Fts5Structure *pStruct = *ppStruct; |
| 222580 | 222700 | int nLevel = pStruct->nLevel; |
| 222581 | 222701 | sqlite3_int64 nByte = ( |
| 222582 | 222702 | sizeof(Fts5Structure) + /* Main structure */ |
| @@ -231175,11 +231295,11 @@ | ||
| 231175 | 231295 | int nArg, /* Number of args */ |
| 231176 | 231296 | sqlite3_value **apUnused /* Function arguments */ |
| 231177 | 231297 | ){ |
| 231178 | 231298 | assert( nArg==0 ); |
| 231179 | 231299 | UNUSED_PARAM2(nArg, apUnused); |
| 231180 | - sqlite3_result_text(pCtx, "fts5: 2021-09-06 11:44:19 b3cfe23bec0b95ca673802526704200e2396df715fdded72aa71addd7f47e0e1", -1, SQLITE_TRANSIENT); | |
| 231300 | + sqlite3_result_text(pCtx, "fts5: 2021-09-22 14:43:35 d678ecca02698753d1b33e072566112e94ea36d0d3a8f4a24d2b09d131968d88", -1, SQLITE_TRANSIENT); | |
| 231181 | 231301 | } |
| 231182 | 231302 | |
| 231183 | 231303 | /* |
| 231184 | 231304 | ** Return true if zName is the extension on one of the shadow tables used |
| 231185 | 231305 | ** by this module. |
| 231186 | 231306 |
| --- src/sqlite3.c | |
| +++ src/sqlite3.c | |
| @@ -452,11 +452,11 @@ | |
| 452 | ** [sqlite3_libversion_number()], [sqlite3_sourceid()], |
| 453 | ** [sqlite_version()] and [sqlite_source_id()]. |
| 454 | */ |
| 455 | #define SQLITE_VERSION "3.37.0" |
| 456 | #define SQLITE_VERSION_NUMBER 3037000 |
| 457 | #define SQLITE_SOURCE_ID "2021-09-06 11:44:19 b3cfe23bec0b95ca673802526704200e2396df715fdded72aa71addd7f47e0e1" |
| 458 | |
| 459 | /* |
| 460 | ** CAPI3REF: Run-Time Library Version Numbers |
| 461 | ** KEYWORDS: sqlite3_version sqlite3_sourceid |
| 462 | ** |
| @@ -19842,11 +19842,11 @@ | |
| 19842 | #ifdef SQLITE_TEST |
| 19843 | SQLITE_PRIVATE int sqlite3Utf8To8(unsigned char*); |
| 19844 | #endif |
| 19845 | |
| 19846 | #ifdef SQLITE_OMIT_VIRTUALTABLE |
| 19847 | # define sqlite3VtabClear(Y) |
| 19848 | # define sqlite3VtabSync(X,Y) SQLITE_OK |
| 19849 | # define sqlite3VtabRollback(X) |
| 19850 | # define sqlite3VtabCommit(X) |
| 19851 | # define sqlite3VtabInSync(db) 0 |
| 19852 | # define sqlite3VtabLock(X) |
| @@ -48856,11 +48856,11 @@ | |
| 48856 | MemStore *p = 0; |
| 48857 | int szName; |
| 48858 | if( (flags & SQLITE_OPEN_MAIN_DB)==0 ){ |
| 48859 | return ORIGVFS(pVfs)->xOpen(ORIGVFS(pVfs), zName, pFd, flags, pOutFlags); |
| 48860 | } |
| 48861 | memset(pFile, 0, sizeof(*p)); |
| 48862 | szName = sqlite3Strlen30(zName); |
| 48863 | if( szName>1 && zName[0]=='/' ){ |
| 48864 | int i; |
| 48865 | #ifndef SQLITE_MUTEX_OMIT |
| 48866 | sqlite3_mutex *pVfsMutex = sqlite3MutexAlloc(SQLITE_MUTEX_STATIC_VFS1); |
| @@ -53160,12 +53160,12 @@ | |
| 53160 | |
| 53161 | u16 nExtra; /* Add this many bytes to each in-memory page */ |
| 53162 | i16 nReserve; /* Number of unused bytes at end of each page */ |
| 53163 | u32 vfsFlags; /* Flags for sqlite3_vfs.xOpen() */ |
| 53164 | u32 sectorSize; /* Assumed sector size during rollback */ |
| 53165 | int pageSize; /* Number of bytes in a page */ |
| 53166 | Pgno mxPgno; /* Maximum allowed size of the database */ |
| 53167 | i64 journalSizeLimit; /* Size limit for persistent journal files */ |
| 53168 | char *zFilename; /* Name of the database file */ |
| 53169 | char *zJournal; /* Name of the journal file */ |
| 53170 | int (*xBusyHandler)(void*); /* Function to call when busy */ |
| 53171 | void *pBusyHandlerArg; /* Context argument for xBusyHandler */ |
| @@ -59218,12 +59218,12 @@ | |
| 59218 | /* |
| 59219 | ** Return the approximate number of bytes of memory currently |
| 59220 | ** used by the pager and its associated cache. |
| 59221 | */ |
| 59222 | SQLITE_PRIVATE int sqlite3PagerMemUsed(Pager *pPager){ |
| 59223 | int perPageSize = pPager->pageSize + pPager->nExtra + sizeof(PgHdr) |
| 59224 | + 5*sizeof(void*); |
| 59225 | return perPageSize*sqlite3PcachePagecount(pPager->pPCache) |
| 59226 | + sqlite3MallocSize(pPager) |
| 59227 | + pPager->pageSize; |
| 59228 | } |
| 59229 | |
| @@ -59413,18 +59413,18 @@ | |
| 59413 | for(ii=nNew; ii<pPager->nSavepoint; ii++){ |
| 59414 | sqlite3BitvecDestroy(pPager->aSavepoint[ii].pInSavepoint); |
| 59415 | } |
| 59416 | pPager->nSavepoint = nNew; |
| 59417 | |
| 59418 | /* If this is a release of the outermost savepoint, truncate |
| 59419 | ** the sub-journal to zero bytes in size. */ |
| 59420 | if( op==SAVEPOINT_RELEASE ){ |
| 59421 | PagerSavepoint *pRel = &pPager->aSavepoint[nNew]; |
| 59422 | if( pRel->bTruncateOnRelease && isOpen(pPager->sjfd) ){ |
| 59423 | /* Only truncate if it is an in-memory sub-journal. */ |
| 59424 | if( sqlite3JournalIsInMemory(pPager->sjfd) ){ |
| 59425 | i64 sz = (pPager->pageSize+4)*pRel->iSubRec; |
| 59426 | rc = sqlite3OsTruncate(pPager->sjfd, sz); |
| 59427 | assert( rc==SQLITE_OK ); |
| 59428 | } |
| 59429 | pPager->nSubRec = pRel->iSubRec; |
| 59430 | } |
| @@ -72713,10 +72713,11 @@ | |
| 72713 | nCell -= nTail; |
| 72714 | } |
| 72715 | |
| 72716 | pData = &aData[get2byteNotZero(&aData[hdr+5])]; |
| 72717 | if( pData<pBegin ) goto editpage_fail; |
| 72718 | |
| 72719 | /* Add cells to the start of the page */ |
| 72720 | if( iNew<iOld ){ |
| 72721 | int nAdd = MIN(nNew,iOld-iNew); |
| 72722 | assert( (iOld-iNew)<nNew || nCell==0 || CORRUPT_DB ); |
| @@ -74118,11 +74119,11 @@ | |
| 74118 | pBt = pPage->pBt; |
| 74119 | ovflPageSize = pBt->usableSize - 4; |
| 74120 | do{ |
| 74121 | rc = btreeGetPage(pBt, ovflPgno, &pPage, 0); |
| 74122 | if( rc ) return rc; |
| 74123 | if( sqlite3PagerPageRefcount(pPage->pDbPage)!=1 ){ |
| 74124 | rc = SQLITE_CORRUPT_BKPT; |
| 74125 | }else{ |
| 74126 | if( iOffset+ovflPageSize<(u32)nTotal ){ |
| 74127 | ovflPgno = get4byte(pPage->aData); |
| 74128 | }else{ |
| @@ -94795,10 +94796,15 @@ | |
| 94795 | rc = SQLITE_NOMEM_BKPT; |
| 94796 | }else if( rc==SQLITE_IOERR_CORRUPTFS ){ |
| 94797 | rc = SQLITE_CORRUPT_BKPT; |
| 94798 | } |
| 94799 | assert( rc ); |
| 94800 | if( p->zErrMsg==0 && rc!=SQLITE_IOERR_NOMEM ){ |
| 94801 | sqlite3VdbeError(p, "%s", sqlite3ErrStr(rc)); |
| 94802 | } |
| 94803 | p->rc = rc; |
| 94804 | sqlite3SystemError(db, rc); |
| @@ -105161,10 +105167,11 @@ | |
| 105161 | |
| 105162 | /*********************************************************************** |
| 105163 | ** Test-only SQL functions that are only usable if enabled |
| 105164 | ** via SQLITE_TESTCTRL_INTERNAL_FUNCTIONS |
| 105165 | */ |
| 105166 | case INLINEFUNC_expr_compare: { |
| 105167 | /* Compare two expressions using sqlite3ExprCompare() */ |
| 105168 | assert( nFarg==2 ); |
| 105169 | sqlite3VdbeAddOp2(v, OP_Integer, |
| 105170 | sqlite3ExprCompare(0,pFarg->a[0].pExpr, pFarg->a[1].pExpr,-1), |
| @@ -105194,11 +105201,10 @@ | |
| 105194 | sqlite3VdbeAddOp2(v, OP_Null, 0, target); |
| 105195 | } |
| 105196 | break; |
| 105197 | } |
| 105198 | |
| 105199 | #ifdef SQLITE_DEBUG |
| 105200 | case INLINEFUNC_affinity: { |
| 105201 | /* The AFFINITY() function evaluates to a string that describes |
| 105202 | ** the type affinity of the argument. This is used for testing of |
| 105203 | ** the SQLite type logic. |
| 105204 | */ |
| @@ -105208,11 +105214,11 @@ | |
| 105208 | aff = sqlite3ExprAffinity(pFarg->a[0].pExpr); |
| 105209 | sqlite3VdbeLoadString(v, target, |
| 105210 | (aff<=SQLITE_AFF_NONE) ? "none" : azAff[aff-SQLITE_AFF_BLOB]); |
| 105211 | break; |
| 105212 | } |
| 105213 | #endif |
| 105214 | } |
| 105215 | return target; |
| 105216 | } |
| 105217 | |
| 105218 | |
| @@ -108172,12 +108178,11 @@ | |
| 108172 | bQuote = sqlite3Isquote(pNew->z[0]); |
| 108173 | sqlite3NestedParse(pParse, |
| 108174 | "UPDATE \"%w\"." DFLT_SCHEMA_TABLE " SET " |
| 108175 | "sql = sqlite_rename_column(sql, type, name, %Q, %Q, %d, %Q, %d, %d) " |
| 108176 | "WHERE name NOT LIKE 'sqliteX_%%' ESCAPE 'X' " |
| 108177 | " AND (type != 'index' OR tbl_name = %Q)" |
| 108178 | " AND sql NOT LIKE 'create virtual%%'", |
| 108179 | zDb, |
| 108180 | zDb, pTab->zName, iCol, zNew, bQuote, iSchema==1, |
| 108181 | pTab->zName |
| 108182 | ); |
| 108183 | |
| @@ -109023,11 +109028,11 @@ | |
| 109023 | rc = (db->mallocFailed ? SQLITE_NOMEM : sParse.rc); |
| 109024 | if( rc==SQLITE_OK ){ |
| 109025 | sqlite3WalkSelect(&sWalker, pSelect); |
| 109026 | } |
| 109027 | if( rc!=SQLITE_OK ) goto renameColumnFunc_done; |
| 109028 | }else if( ALWAYS(IsOrdinaryTable(sParse.pNewTable)) ){ |
| 109029 | /* A regular table */ |
| 109030 | int bFKOnly = sqlite3_stricmp(zTable, sParse.pNewTable->zName); |
| 109031 | FKey *pFKey; |
| 109032 | sCtx.pTab = sParse.pNewTable; |
| 109033 | if( bFKOnly==0 ){ |
| @@ -120209,13 +120214,13 @@ | |
| 120209 | } |
| 120210 | |
| 120211 | /* |
| 120212 | ** Implementation of the changes() SQL function. |
| 120213 | ** |
| 120214 | ** IMP: R-62073-11209 The changes() SQL function is a wrapper |
| 120215 | ** around the sqlite3_changes64() C/C++ function and hence follows the same |
| 120216 | ** rules for counting changes. |
| 120217 | */ |
| 120218 | static void changes( |
| 120219 | sqlite3_context *context, |
| 120220 | int NotUsed, |
| 120221 | sqlite3_value **NotUsed2 |
| @@ -120234,12 +120239,12 @@ | |
| 120234 | int NotUsed, |
| 120235 | sqlite3_value **NotUsed2 |
| 120236 | ){ |
| 120237 | sqlite3 *db = sqlite3_context_db_handle(context); |
| 120238 | UNUSED_PARAMETER2(NotUsed, NotUsed2); |
| 120239 | /* IMP: R-52756-41993 This function was a wrapper around the |
| 120240 | ** sqlite3_total_changes() C/C++ interface. */ |
| 120241 | sqlite3_result_int64(context, sqlite3_total_changes64(db)); |
| 120242 | } |
| 120243 | |
| 120244 | /* |
| 120245 | ** A structure defining how to do GLOB-style comparisons. |
| @@ -121761,16 +121766,16 @@ | |
| 121761 | ** |
| 121762 | ** For peak efficiency, put the most frequently used function last. |
| 121763 | */ |
| 121764 | static FuncDef aBuiltinFunc[] = { |
| 121765 | /***** Functions only available with SQLITE_TESTCTRL_INTERNAL_FUNCTIONS *****/ |
| 121766 | TEST_FUNC(implies_nonnull_row, 2, INLINEFUNC_implies_nonnull_row, 0), |
| 121767 | TEST_FUNC(expr_compare, 2, INLINEFUNC_expr_compare, 0), |
| 121768 | TEST_FUNC(expr_implies_expr, 2, INLINEFUNC_expr_implies_expr, 0), |
| 121769 | #ifdef SQLITE_DEBUG |
| 121770 | TEST_FUNC(affinity, 1, INLINEFUNC_affinity, 0), |
| 121771 | #endif |
| 121772 | /***** Regular functions *****/ |
| 121773 | #ifdef SQLITE_SOUNDEX |
| 121774 | FUNCTION(soundex, 1, 0, 0, soundexFunc ), |
| 121775 | #endif |
| 121776 | #ifndef SQLITE_OMIT_LOAD_EXTENSION |
| @@ -128264,17 +128269,18 @@ | |
| 128264 | #define PragTyp_SECURE_DELETE 33 |
| 128265 | #define PragTyp_SHRINK_MEMORY 34 |
| 128266 | #define PragTyp_SOFT_HEAP_LIMIT 35 |
| 128267 | #define PragTyp_SYNCHRONOUS 36 |
| 128268 | #define PragTyp_TABLE_INFO 37 |
| 128269 | #define PragTyp_TEMP_STORE 38 |
| 128270 | #define PragTyp_TEMP_STORE_DIRECTORY 39 |
| 128271 | #define PragTyp_THREADS 40 |
| 128272 | #define PragTyp_WAL_AUTOCHECKPOINT 41 |
| 128273 | #define PragTyp_WAL_CHECKPOINT 42 |
| 128274 | #define PragTyp_LOCK_STATUS 43 |
| 128275 | #define PragTyp_STATS 44 |
| 128276 | |
| 128277 | /* Property flags associated with various pragma. */ |
| 128278 | #define PragFlg_NeedSchema 0x01 /* Force schema load before running */ |
| 128279 | #define PragFlg_NoColumns 0x02 /* OP_ResultRow called with zero columns */ |
| 128280 | #define PragFlg_NoColumns1 0x04 /* zero columns if RHS argument is present */ |
| @@ -128303,49 +128309,55 @@ | |
| 128303 | /* 11 */ "notnull", |
| 128304 | /* 12 */ "dflt_value", |
| 128305 | /* 13 */ "pk", |
| 128306 | /* 14 */ "hidden", |
| 128307 | /* table_info reuses 8 */ |
| 128308 | /* 15 */ "seqno", /* Used by: index_xinfo */ |
| 128309 | /* 16 */ "cid", |
| 128310 | /* 17 */ "name", |
| 128311 | /* 18 */ "desc", |
| 128312 | /* 19 */ "coll", |
| 128313 | /* 20 */ "key", |
| 128314 | /* 21 */ "name", /* Used by: function_list */ |
| 128315 | /* 22 */ "builtin", |
| 128316 | /* 23 */ "type", |
| 128317 | /* 24 */ "enc", |
| 128318 | /* 25 */ "narg", |
| 128319 | /* 26 */ "flags", |
| 128320 | /* 27 */ "tbl", /* Used by: stats */ |
| 128321 | /* 28 */ "idx", |
| 128322 | /* 29 */ "wdth", |
| 128323 | /* 30 */ "hght", |
| 128324 | /* 31 */ "flgs", |
| 128325 | /* 32 */ "seq", /* Used by: index_list */ |
| 128326 | /* 33 */ "name", |
| 128327 | /* 34 */ "unique", |
| 128328 | /* 35 */ "origin", |
| 128329 | /* 36 */ "partial", |
| 128330 | /* 37 */ "table", /* Used by: foreign_key_check */ |
| 128331 | /* 38 */ "rowid", |
| 128332 | /* 39 */ "parent", |
| 128333 | /* 40 */ "fkid", |
| 128334 | /* index_info reuses 15 */ |
| 128335 | /* 41 */ "seq", /* Used by: database_list */ |
| 128336 | /* 42 */ "name", |
| 128337 | /* 43 */ "file", |
| 128338 | /* 44 */ "busy", /* Used by: wal_checkpoint */ |
| 128339 | /* 45 */ "log", |
| 128340 | /* 46 */ "checkpointed", |
| 128341 | /* collation_list reuses 32 */ |
| 128342 | /* 47 */ "database", /* Used by: lock_status */ |
| 128343 | /* 48 */ "status", |
| 128344 | /* 49 */ "cache_size", /* Used by: default_cache_size */ |
| 128345 | /* module_list pragma_list reuses 9 */ |
| 128346 | /* 50 */ "timeout", /* Used by: busy_timeout */ |
| 128347 | }; |
| 128348 | |
| 128349 | /* Definitions of all built-in pragmas */ |
| 128350 | typedef struct PragmaName { |
| 128351 | const char *const zName; /* Name of pragma */ |
| @@ -128392,11 +128404,11 @@ | |
| 128392 | #endif |
| 128393 | #endif |
| 128394 | {/* zName: */ "busy_timeout", |
| 128395 | /* ePragTyp: */ PragTyp_BUSY_TIMEOUT, |
| 128396 | /* ePragFlg: */ PragFlg_Result0, |
| 128397 | /* ColNames: */ 50, 1, |
| 128398 | /* iArg: */ 0 }, |
| 128399 | #if !defined(SQLITE_OMIT_PAGER_PRAGMAS) |
| 128400 | {/* zName: */ "cache_size", |
| 128401 | /* ePragTyp: */ PragTyp_CACHE_SIZE, |
| 128402 | /* ePragFlg: */ PragFlg_NeedSchema|PragFlg_Result0|PragFlg_SchemaReq|PragFlg_NoColumns1, |
| @@ -128431,11 +128443,11 @@ | |
| 128431 | #endif |
| 128432 | #if !defined(SQLITE_OMIT_SCHEMA_PRAGMAS) |
| 128433 | {/* zName: */ "collation_list", |
| 128434 | /* ePragTyp: */ PragTyp_COLLATION_LIST, |
| 128435 | /* ePragFlg: */ PragFlg_Result0, |
| 128436 | /* ColNames: */ 32, 2, |
| 128437 | /* iArg: */ 0 }, |
| 128438 | #endif |
| 128439 | #if !defined(SQLITE_OMIT_COMPILEOPTION_DIAGS) |
| 128440 | {/* zName: */ "compile_options", |
| 128441 | /* ePragTyp: */ PragTyp_COMPILE_OPTIONS, |
| @@ -128466,18 +128478,18 @@ | |
| 128466 | #endif |
| 128467 | #if !defined(SQLITE_OMIT_SCHEMA_PRAGMAS) |
| 128468 | {/* zName: */ "database_list", |
| 128469 | /* ePragTyp: */ PragTyp_DATABASE_LIST, |
| 128470 | /* ePragFlg: */ PragFlg_NeedSchema|PragFlg_Result0, |
| 128471 | /* ColNames: */ 41, 3, |
| 128472 | /* iArg: */ 0 }, |
| 128473 | #endif |
| 128474 | #if !defined(SQLITE_OMIT_PAGER_PRAGMAS) && !defined(SQLITE_OMIT_DEPRECATED) |
| 128475 | {/* zName: */ "default_cache_size", |
| 128476 | /* ePragTyp: */ PragTyp_DEFAULT_CACHE_SIZE, |
| 128477 | /* ePragFlg: */ PragFlg_NeedSchema|PragFlg_Result0|PragFlg_SchemaReq|PragFlg_NoColumns1, |
| 128478 | /* ColNames: */ 49, 1, |
| 128479 | /* iArg: */ 0 }, |
| 128480 | #endif |
| 128481 | #if !defined(SQLITE_OMIT_FLAG_PRAGMAS) |
| 128482 | #if !defined(SQLITE_OMIT_FOREIGN_KEY) && !defined(SQLITE_OMIT_TRIGGER) |
| 128483 | {/* zName: */ "defer_foreign_keys", |
| @@ -128503,11 +128515,11 @@ | |
| 128503 | #endif |
| 128504 | #if !defined(SQLITE_OMIT_FOREIGN_KEY) && !defined(SQLITE_OMIT_TRIGGER) |
| 128505 | {/* zName: */ "foreign_key_check", |
| 128506 | /* ePragTyp: */ PragTyp_FOREIGN_KEY_CHECK, |
| 128507 | /* ePragFlg: */ PragFlg_NeedSchema|PragFlg_Result0|PragFlg_Result1|PragFlg_SchemaOpt, |
| 128508 | /* ColNames: */ 37, 4, |
| 128509 | /* iArg: */ 0 }, |
| 128510 | #endif |
| 128511 | #if !defined(SQLITE_OMIT_FOREIGN_KEY) |
| 128512 | {/* zName: */ "foreign_key_list", |
| 128513 | /* ePragTyp: */ PragTyp_FOREIGN_KEY_LIST, |
| @@ -128546,11 +128558,11 @@ | |
| 128546 | #if !defined(SQLITE_OMIT_SCHEMA_PRAGMAS) |
| 128547 | #if !defined(SQLITE_OMIT_INTROSPECTION_PRAGMAS) |
| 128548 | {/* zName: */ "function_list", |
| 128549 | /* ePragTyp: */ PragTyp_FUNCTION_LIST, |
| 128550 | /* ePragFlg: */ PragFlg_Result0, |
| 128551 | /* ColNames: */ 21, 6, |
| 128552 | /* iArg: */ 0 }, |
| 128553 | #endif |
| 128554 | #endif |
| 128555 | {/* zName: */ "hard_heap_limit", |
| 128556 | /* ePragTyp: */ PragTyp_HARD_HEAP_LIMIT, |
| @@ -128575,21 +128587,21 @@ | |
| 128575 | #endif |
| 128576 | #if !defined(SQLITE_OMIT_SCHEMA_PRAGMAS) |
| 128577 | {/* zName: */ "index_info", |
| 128578 | /* ePragTyp: */ PragTyp_INDEX_INFO, |
| 128579 | /* ePragFlg: */ PragFlg_NeedSchema|PragFlg_Result1|PragFlg_SchemaOpt, |
| 128580 | /* ColNames: */ 15, 3, |
| 128581 | /* iArg: */ 0 }, |
| 128582 | {/* zName: */ "index_list", |
| 128583 | /* ePragTyp: */ PragTyp_INDEX_LIST, |
| 128584 | /* ePragFlg: */ PragFlg_NeedSchema|PragFlg_Result1|PragFlg_SchemaOpt, |
| 128585 | /* ColNames: */ 32, 5, |
| 128586 | /* iArg: */ 0 }, |
| 128587 | {/* zName: */ "index_xinfo", |
| 128588 | /* ePragTyp: */ PragTyp_INDEX_INFO, |
| 128589 | /* ePragFlg: */ PragFlg_NeedSchema|PragFlg_Result1|PragFlg_SchemaOpt, |
| 128590 | /* ColNames: */ 15, 6, |
| 128591 | /* iArg: */ 1 }, |
| 128592 | #endif |
| 128593 | #if !defined(SQLITE_OMIT_INTEGRITY_CHECK) |
| 128594 | {/* zName: */ "integrity_check", |
| 128595 | /* ePragTyp: */ PragTyp_INTEGRITY_CHECK, |
| @@ -128625,11 +128637,11 @@ | |
| 128625 | #endif |
| 128626 | #if defined(SQLITE_DEBUG) || defined(SQLITE_TEST) |
| 128627 | {/* zName: */ "lock_status", |
| 128628 | /* ePragTyp: */ PragTyp_LOCK_STATUS, |
| 128629 | /* ePragFlg: */ PragFlg_Result0, |
| 128630 | /* ColNames: */ 47, 2, |
| 128631 | /* iArg: */ 0 }, |
| 128632 | #endif |
| 128633 | #if !defined(SQLITE_OMIT_PAGER_PRAGMAS) |
| 128634 | {/* zName: */ "locking_mode", |
| 128635 | /* ePragTyp: */ PragTyp_LOCKING_MODE, |
| @@ -128764,11 +128776,11 @@ | |
| 128764 | #endif |
| 128765 | #if !defined(SQLITE_OMIT_SCHEMA_PRAGMAS) && defined(SQLITE_DEBUG) |
| 128766 | {/* zName: */ "stats", |
| 128767 | /* ePragTyp: */ PragTyp_STATS, |
| 128768 | /* ePragFlg: */ PragFlg_NeedSchema|PragFlg_Result0|PragFlg_SchemaReq, |
| 128769 | /* ColNames: */ 27, 5, |
| 128770 | /* iArg: */ 0 }, |
| 128771 | #endif |
| 128772 | #if !defined(SQLITE_OMIT_PAGER_PRAGMAS) |
| 128773 | {/* zName: */ "synchronous", |
| 128774 | /* ePragTyp: */ PragTyp_SYNCHRONOUS, |
| @@ -128780,10 +128792,15 @@ | |
| 128780 | {/* zName: */ "table_info", |
| 128781 | /* ePragTyp: */ PragTyp_TABLE_INFO, |
| 128782 | /* ePragFlg: */ PragFlg_NeedSchema|PragFlg_Result1|PragFlg_SchemaOpt, |
| 128783 | /* ColNames: */ 8, 6, |
| 128784 | /* iArg: */ 0 }, |
| 128785 | {/* zName: */ "table_xinfo", |
| 128786 | /* ePragTyp: */ PragTyp_TABLE_INFO, |
| 128787 | /* ePragFlg: */ PragFlg_NeedSchema|PragFlg_Result1|PragFlg_SchemaOpt, |
| 128788 | /* ColNames: */ 8, 7, |
| 128789 | /* iArg: */ 1 }, |
| @@ -128855,11 +128872,11 @@ | |
| 128855 | /* ColNames: */ 0, 0, |
| 128856 | /* iArg: */ 0 }, |
| 128857 | {/* zName: */ "wal_checkpoint", |
| 128858 | /* ePragTyp: */ PragTyp_WAL_CHECKPOINT, |
| 128859 | /* ePragFlg: */ PragFlg_NeedSchema, |
| 128860 | /* ColNames: */ 44, 3, |
| 128861 | /* iArg: */ 0 }, |
| 128862 | #endif |
| 128863 | #if !defined(SQLITE_OMIT_FLAG_PRAGMAS) |
| 128864 | {/* zName: */ "writable_schema", |
| 128865 | /* ePragTyp: */ PragTyp_FLAG, |
| @@ -128866,11 +128883,11 @@ | |
| 128866 | /* ePragFlg: */ PragFlg_Result0|PragFlg_NoColumns1, |
| 128867 | /* ColNames: */ 0, 0, |
| 128868 | /* iArg: */ SQLITE_WriteSchema|SQLITE_NoSchemaError }, |
| 128869 | #endif |
| 128870 | }; |
| 128871 | /* Number of pragmas: 67 on by default, 77 total. */ |
| 128872 | |
| 128873 | /************** End of pragma.h **********************************************/ |
| 128874 | /************** Continuing where we left off in pragma.c *********************/ |
| 128875 | |
| 128876 | /* |
| @@ -130035,10 +130052,58 @@ | |
| 130035 | } |
| 130036 | } |
| 130037 | } |
| 130038 | break; |
| 130039 | |
| 130040 | #ifdef SQLITE_DEBUG |
| 130041 | case PragTyp_STATS: { |
| 130042 | Index *pIdx; |
| 130043 | HashElem *i; |
| 130044 | pParse->nMem = 5; |
| @@ -130544,11 +130609,13 @@ | |
| 130544 | }else{ |
| 130545 | integrityCheckResultRow(v); |
| 130546 | } |
| 130547 | sqlite3VdbeJumpHere(v, jmp2); |
| 130548 | } |
| 130549 | if( pTab->tabFlags & TF_Strict ){ |
| 130550 | jmp2 = sqlite3VdbeAddOp3(v, OP_IsNullOrType, 3, 0, |
| 130551 | sqlite3StdTypeMap[pCol->eCType-1]); |
| 130552 | VdbeCoverage(v); |
| 130553 | zErr = sqlite3MPrintf(db, "non-%s value in %s.%s", |
| 130554 | sqlite3StdType[pCol->eCType-1], |
| @@ -132836,10 +132903,13 @@ | |
| 132836 | |
| 132837 | pE1 = sqlite3CreateColumnExpr(db, pSrc, iLeft, iColLeft); |
| 132838 | pE2 = sqlite3CreateColumnExpr(db, pSrc, iRight, iColRight); |
| 132839 | |
| 132840 | pEq = sqlite3PExpr(pParse, TK_EQ, pE1, pE2); |
| 132841 | if( pEq && isOuterJoin ){ |
| 132842 | ExprSetProperty(pEq, EP_FromJoin); |
| 132843 | assert( !ExprHasProperty(pEq, EP_TokenOnly|EP_Reduced) ); |
| 132844 | ExprSetVVAProperty(pEq, EP_NoReduce); |
| 132845 | pEq->iRightJoinTable = pE2->iTable; |
| @@ -148365,11 +148435,11 @@ | |
| 148365 | ** 2019-06-14 https://sqlite.org/src/info/ce8717f0885af975 |
| 148366 | ** 2019-09-03 https://sqlite.org/src/info/0f0428096f17252a |
| 148367 | */ |
| 148368 | if( pLeft->op!=TK_COLUMN |
| 148369 | || sqlite3ExprAffinity(pLeft)!=SQLITE_AFF_TEXT |
| 148370 | || IsVirtual(pLeft->y.pTab) /* Value might be numeric */ |
| 148371 | ){ |
| 148372 | int isNum; |
| 148373 | double rDummy; |
| 148374 | isNum = sqlite3AtoF(zNew, &rDummy, iTo, SQLITE_UTF8); |
| 148375 | if( isNum<=0 ){ |
| @@ -156490,10 +156560,13 @@ | |
| 156490 | ); |
| 156491 | SELECTTRACE(1,pParse,pSub, |
| 156492 | ("New window-function subquery in FROM clause of (%u/%p)\n", |
| 156493 | p->selId, p)); |
| 156494 | p->pSrc = sqlite3SrcListAppend(pParse, 0, 0, 0); |
| 156495 | if( p->pSrc ){ |
| 156496 | Table *pTab2; |
| 156497 | p->pSrc->a[0].pSelect = pSub; |
| 156498 | sqlite3SrcListAssignCursors(pParse, p->pSrc); |
| 156499 | pSub->selFlags |= SF_Expanded|SF_OrderByReqd; |
| @@ -192908,11 +192981,15 @@ | |
| 192908 | #else |
| 192909 | /* #include "sqlite3.h" */ |
| 192910 | #endif |
| 192911 | SQLITE_PRIVATE int sqlite3GetToken(const unsigned char*,int*); /* In the SQLite core */ |
| 192912 | |
| 192913 | #ifndef SQLITE_AMALGAMATION |
| 192914 | #include "sqlite3rtree.h" |
| 192915 | typedef sqlite3_int64 i64; |
| 192916 | typedef sqlite3_uint64 u64; |
| 192917 | typedef unsigned char u8; |
| 192918 | typedef unsigned short u16; |
| @@ -192921,11 +192998,21 @@ | |
| 192921 | # define NDEBUG 1 |
| 192922 | #endif |
| 192923 | #if defined(NDEBUG) && defined(SQLITE_DEBUG) |
| 192924 | # undef NDEBUG |
| 192925 | #endif |
| 192926 | #endif |
| 192927 | |
| 192928 | /* #include <string.h> */ |
| 192929 | /* #include <stdio.h> */ |
| 192930 | /* #include <assert.h> */ |
| 192931 | /* #include <stdlib.h> */ |
| @@ -192979,11 +193066,13 @@ | |
| 192979 | u8 nDim2; /* Twice the number of dimensions */ |
| 192980 | u8 eCoordType; /* RTREE_COORD_REAL32 or RTREE_COORD_INT32 */ |
| 192981 | u8 nBytesPerCell; /* Bytes consumed per cell */ |
| 192982 | u8 inWrTrans; /* True if inside write transaction */ |
| 192983 | u8 nAux; /* # of auxiliary columns in %_rowid */ |
| 192984 | u8 nAuxNotNull; /* Number of initial not-null aux columns */ |
| 192985 | #ifdef SQLITE_DEBUG |
| 192986 | u8 bCorrupt; /* Shadow table corruption detected */ |
| 192987 | #endif |
| 192988 | int iDepth; /* Current depth of the r-tree structure */ |
| 192989 | char *zDb; /* Name of database containing r-tree table */ |
| @@ -193510,22 +193599,10 @@ | |
| 193510 | pRtree->pNodeBlob = 0; |
| 193511 | sqlite3_blob_close(pBlob); |
| 193512 | } |
| 193513 | } |
| 193514 | |
| 193515 | /* |
| 193516 | ** Check to see if pNode is the same as pParent or any of the parents |
| 193517 | ** of pParent. |
| 193518 | */ |
| 193519 | static int nodeInParentChain(const RtreeNode *pNode, const RtreeNode *pParent){ |
| 193520 | do{ |
| 193521 | if( pNode==pParent ) return 1; |
| 193522 | pParent = pParent->pParent; |
| 193523 | }while( pParent ); |
| 193524 | return 0; |
| 193525 | } |
| 193526 | |
| 193527 | /* |
| 193528 | ** Obtain a reference to an r-tree node. |
| 193529 | */ |
| 193530 | static int nodeAcquire( |
| 193531 | Rtree *pRtree, /* R-tree structure */ |
| @@ -193538,18 +193615,11 @@ | |
| 193538 | |
| 193539 | /* Check if the requested node is already in the hash table. If so, |
| 193540 | ** increase its reference count and return it. |
| 193541 | */ |
| 193542 | if( (pNode = nodeHashLookup(pRtree, iNode))!=0 ){ |
| 193543 | if( pParent && !pNode->pParent ){ |
| 193544 | if( nodeInParentChain(pNode, pParent) ){ |
| 193545 | RTREE_IS_CORRUPT(pRtree); |
| 193546 | return SQLITE_CORRUPT_VTAB; |
| 193547 | } |
| 193548 | pParent->nRef++; |
| 193549 | pNode->pParent = pParent; |
| 193550 | }else if( pParent && pNode->pParent && pParent!=pNode->pParent ){ |
| 193551 | RTREE_IS_CORRUPT(pRtree); |
| 193552 | return SQLITE_CORRUPT_VTAB; |
| 193553 | } |
| 193554 | pNode->nRef++; |
| 193555 | *ppNode = pNode; |
| @@ -193603,11 +193673,11 @@ | |
| 193603 | ** of the r-tree structure. A height of zero means all data is stored on |
| 193604 | ** the root node. A height of one means the children of the root node |
| 193605 | ** are the leaves, and so on. If the depth as specified on the root node |
| 193606 | ** is greater than RTREE_MAX_DEPTH, the r-tree structure must be corrupt. |
| 193607 | */ |
| 193608 | if( pNode && rc==SQLITE_OK && iNode==1 ){ |
| 193609 | pRtree->iDepth = readInt16(pNode->zData); |
| 193610 | if( pRtree->iDepth>RTREE_MAX_DEPTH ){ |
| 193611 | rc = SQLITE_CORRUPT_VTAB; |
| 193612 | RTREE_IS_CORRUPT(pRtree); |
| 193613 | } |
| @@ -194209,15 +194279,16 @@ | |
| 194209 | ** Return the index of the cell containing a pointer to node pNode |
| 194210 | ** in its parent. If pNode is the root node, return -1. |
| 194211 | */ |
| 194212 | static int nodeParentIndex(Rtree *pRtree, RtreeNode *pNode, int *piIndex){ |
| 194213 | RtreeNode *pParent = pNode->pParent; |
| 194214 | if( pParent ){ |
| 194215 | return nodeRowidIndex(pRtree, pParent, pNode->iNode, piIndex); |
| 194216 | } |
| 194217 | *piIndex = -1; |
| 194218 | return SQLITE_OK; |
| 194219 | } |
| 194220 | |
| 194221 | /* |
| 194222 | ** Compare two search points. Return negative, zero, or positive if the first |
| 194223 | ** is less than, equal to, or greater than the second. |
| @@ -194336,11 +194407,12 @@ | |
| 194336 | if( pCur->bPoint ){ |
| 194337 | int ii; |
| 194338 | pNew = rtreeEnqueue(pCur, rScore, iLevel); |
| 194339 | if( pNew==0 ) return 0; |
| 194340 | ii = (int)(pNew - pCur->aPoint) + 1; |
| 194341 | if( ii<RTREE_CACHE_SZ ){ |
| 194342 | assert( pCur->aNode[ii]==0 ); |
| 194343 | pCur->aNode[ii] = pCur->aNode[0]; |
| 194344 | }else{ |
| 194345 | nodeRelease(RTREE_OF_CURSOR(pCur), pCur->aNode[0]); |
| 194346 | } |
| @@ -194397,11 +194469,11 @@ | |
| 194397 | p->aNode[i] = 0; |
| 194398 | } |
| 194399 | if( p->bPoint ){ |
| 194400 | p->anQueue[p->sPoint.iLevel]--; |
| 194401 | p->bPoint = 0; |
| 194402 | }else if( p->nPoint ){ |
| 194403 | p->anQueue[p->aPoint[0].iLevel]--; |
| 194404 | n = --p->nPoint; |
| 194405 | p->aPoint[0] = p->aPoint[n]; |
| 194406 | if( n<RTREE_CACHE_SZ-1 ){ |
| 194407 | p->aNode[1] = p->aNode[n+1]; |
| @@ -194538,11 +194610,11 @@ | |
| 194538 | static int rtreeRowid(sqlite3_vtab_cursor *pVtabCursor, sqlite_int64 *pRowid){ |
| 194539 | RtreeCursor *pCsr = (RtreeCursor *)pVtabCursor; |
| 194540 | RtreeSearchPoint *p = rtreeSearchPointFirst(pCsr); |
| 194541 | int rc = SQLITE_OK; |
| 194542 | RtreeNode *pNode = rtreeNodeOfFirstSearchPoint(pCsr, &rc); |
| 194543 | if( rc==SQLITE_OK && p ){ |
| 194544 | *pRowid = nodeGetRowid(RTREE_OF_CURSOR(pCsr), pNode, p->iCell); |
| 194545 | } |
| 194546 | return rc; |
| 194547 | } |
| 194548 | |
| @@ -194556,11 +194628,11 @@ | |
| 194556 | RtreeCoord c; |
| 194557 | int rc = SQLITE_OK; |
| 194558 | RtreeNode *pNode = rtreeNodeOfFirstSearchPoint(pCsr, &rc); |
| 194559 | |
| 194560 | if( rc ) return rc; |
| 194561 | if( p==0 ) return SQLITE_OK; |
| 194562 | if( i==0 ){ |
| 194563 | sqlite3_result_int64(ctx, nodeGetRowid(pRtree, pNode, p->iCell)); |
| 194564 | }else if( i<=pRtree->nDim2 ){ |
| 194565 | nodeGetCoord(pRtree, pNode, p->iCell, i-1, &c); |
| 194566 | #ifndef SQLITE_RTREE_INT_ONLY |
| @@ -194755,12 +194827,15 @@ | |
| 194755 | } |
| 194756 | } |
| 194757 | } |
| 194758 | if( rc==SQLITE_OK ){ |
| 194759 | RtreeSearchPoint *pNew; |
| 194760 | pNew = rtreeSearchPointNew(pCsr, RTREE_ZERO, (u8)(pRtree->iDepth+1)); |
| 194761 | if( pNew==0 ) return SQLITE_NOMEM; |
| 194762 | pNew->id = 1; |
| 194763 | pNew->iCell = 0; |
| 194764 | pNew->eWithin = PARTLY_WITHIN; |
| 194765 | assert( pCsr->bPoint==1 ); |
| 194766 | pCsr->aNode[0] = pRoot; |
| @@ -194833,11 +194908,11 @@ | |
| 194833 | assert( pIdxInfo->idxStr==0 ); |
| 194834 | for(ii=0; ii<pIdxInfo->nConstraint && iIdx<(int)(sizeof(zIdxStr)-1); ii++){ |
| 194835 | struct sqlite3_index_constraint *p = &pIdxInfo->aConstraint[ii]; |
| 194836 | |
| 194837 | if( bMatch==0 && p->usable |
| 194838 | && p->iColumn==0 && p->op==SQLITE_INDEX_CONSTRAINT_EQ |
| 194839 | ){ |
| 194840 | /* We have an equality constraint on the rowid. Use strategy 1. */ |
| 194841 | int jj; |
| 194842 | for(jj=0; jj<ii; jj++){ |
| 194843 | pIdxInfo->aConstraintUsage[jj].argvIndex = 0; |
| @@ -195086,16 +195161,23 @@ | |
| 195086 | RtreeNode *pNode, /* Adjust ancestry of this node. */ |
| 195087 | RtreeCell *pCell /* This cell was just inserted */ |
| 195088 | ){ |
| 195089 | RtreeNode *p = pNode; |
| 195090 | int cnt = 0; |
| 195091 | while( p->pParent ){ |
| 195092 | RtreeNode *pParent = p->pParent; |
| 195093 | RtreeCell cell; |
| 195094 | int iCell; |
| 195095 | |
| 195096 | if( (++cnt)>1000 || nodeParentIndex(pRtree, p, &iCell) ){ |
| 195097 | RTREE_IS_CORRUPT(pRtree); |
| 195098 | return SQLITE_CORRUPT_VTAB; |
| 195099 | } |
| 195100 | |
| 195101 | nodeGetCell(pRtree, pParent, iCell, &cell); |
| @@ -195475,15 +195557,16 @@ | |
| 195475 | } |
| 195476 | }else{ |
| 195477 | RtreeNode *pParent = pLeft->pParent; |
| 195478 | int iCell; |
| 195479 | rc = nodeParentIndex(pRtree, pLeft, &iCell); |
| 195480 | if( rc==SQLITE_OK ){ |
| 195481 | nodeOverwriteCell(pRtree, pParent, &leftbbox, iCell); |
| 195482 | rc = AdjustTree(pRtree, pParent, &leftbbox); |
| 195483 | } |
| 195484 | if( rc!=SQLITE_OK ){ |
| 195485 | goto splitnode_out; |
| 195486 | } |
| 195487 | } |
| 195488 | if( (rc = rtreeInsertCell(pRtree, pRight->pParent, &rightbbox, iHeight+1)) ){ |
| 195489 | goto splitnode_out; |
| @@ -195554,11 +195637,11 @@ | |
| 195554 | ** want to do this as it leads to a memory leak when trying to delete |
| 195555 | ** the referenced counted node structures. |
| 195556 | */ |
| 195557 | iNode = sqlite3_column_int64(pRtree->pReadParent, 0); |
| 195558 | for(pTest=pLeaf; pTest && pTest->iNode!=iNode; pTest=pTest->pParent); |
| 195559 | if( !pTest ){ |
| 195560 | rc2 = nodeAcquire(pRtree, iNode, 0, &pChild->pParent); |
| 195561 | } |
| 195562 | } |
| 195563 | rc = sqlite3_reset(pRtree->pReadParent); |
| 195564 | if( rc==SQLITE_OK ) rc = rc2; |
| @@ -195585,10 +195668,11 @@ | |
| 195585 | rc = nodeParentIndex(pRtree, pNode, &iCell); |
| 195586 | if( rc==SQLITE_OK ){ |
| 195587 | pParent = pNode->pParent; |
| 195588 | pNode->pParent = 0; |
| 195589 | rc = deleteCell(pRtree, pParent, iCell, iHeight+1); |
| 195590 | } |
| 195591 | rc2 = nodeRelease(pRtree, pParent); |
| 195592 | if( rc==SQLITE_OK ){ |
| 195593 | rc = rc2; |
| 195594 | } |
| @@ -195807,11 +195891,11 @@ | |
| 195807 | pRtree->iReinsertHeight = iHeight; |
| 195808 | rc = Reinsert(pRtree, pNode, pCell, iHeight); |
| 195809 | } |
| 195810 | }else{ |
| 195811 | rc = AdjustTree(pRtree, pNode, pCell); |
| 195812 | if( rc==SQLITE_OK ){ |
| 195813 | if( iHeight==0 ){ |
| 195814 | rc = rowidWrite(pRtree, pCell->iRowid, pNode->iNode); |
| 195815 | }else{ |
| 195816 | rc = parentWrite(pRtree, pCell->iRowid, pNode->iNode); |
| 195817 | } |
| @@ -195913,11 +195997,11 @@ | |
| 195913 | */ |
| 195914 | if( rc==SQLITE_OK && pRtree->iDepth>0 && NCELL(pRoot)==1 ){ |
| 195915 | int rc2; |
| 195916 | RtreeNode *pChild = 0; |
| 195917 | i64 iChild = nodeGetRowid(pRtree, pRoot, 0); |
| 195918 | rc = nodeAcquire(pRtree, iChild, pRoot, &pChild); |
| 195919 | if( rc==SQLITE_OK ){ |
| 195920 | rc = removeNode(pRtree, pChild, pRtree->iDepth-1); |
| 195921 | } |
| 195922 | rc2 = nodeRelease(pRtree, pChild); |
| 195923 | if( rc==SQLITE_OK ) rc = rc2; |
| @@ -196248,11 +196332,11 @@ | |
| 196248 | static int rtreeQueryStat1(sqlite3 *db, Rtree *pRtree){ |
| 196249 | const char *zFmt = "SELECT stat FROM %Q.sqlite_stat1 WHERE tbl = '%q_rowid'"; |
| 196250 | char *zSql; |
| 196251 | sqlite3_stmt *p; |
| 196252 | int rc; |
| 196253 | i64 nRow = 0; |
| 196254 | |
| 196255 | rc = sqlite3_table_column_metadata( |
| 196256 | db, pRtree->zDb, "sqlite_stat1",0,0,0,0,0,0 |
| 196257 | ); |
| 196258 | if( rc!=SQLITE_OK ){ |
| @@ -196265,24 +196349,14 @@ | |
| 196265 | }else{ |
| 196266 | rc = sqlite3_prepare_v2(db, zSql, -1, &p, 0); |
| 196267 | if( rc==SQLITE_OK ){ |
| 196268 | if( sqlite3_step(p)==SQLITE_ROW ) nRow = sqlite3_column_int64(p, 0); |
| 196269 | rc = sqlite3_finalize(p); |
| 196270 | }else if( rc!=SQLITE_NOMEM ){ |
| 196271 | rc = SQLITE_OK; |
| 196272 | } |
| 196273 | |
| 196274 | if( rc==SQLITE_OK ){ |
| 196275 | if( nRow==0 ){ |
| 196276 | pRtree->nRowEst = RTREE_DEFAULT_ROWEST; |
| 196277 | }else{ |
| 196278 | pRtree->nRowEst = MAX(nRow, RTREE_MIN_ROWEST); |
| 196279 | } |
| 196280 | } |
| 196281 | sqlite3_free(zSql); |
| 196282 | } |
| 196283 | |
| 196284 | return rc; |
| 196285 | } |
| 196286 | |
| 196287 | |
| 196288 | /* |
| @@ -196428,13 +196502,16 @@ | |
| 196428 | int ii; |
| 196429 | char *zSql; |
| 196430 | sqlite3_str_appendf(p, "UPDATE \"%w\".\"%w_rowid\"SET ", zDb, zPrefix); |
| 196431 | for(ii=0; ii<pRtree->nAux; ii++){ |
| 196432 | if( ii ) sqlite3_str_append(p, ",", 1); |
| 196433 | if( ii<pRtree->nAuxNotNull ){ |
| 196434 | sqlite3_str_appendf(p,"a%d=coalesce(?%d,a%d)",ii,ii+2,ii); |
| 196435 | }else{ |
| 196436 | sqlite3_str_appendf(p,"a%d=?%d",ii,ii+2); |
| 196437 | } |
| 196438 | } |
| 196439 | sqlite3_str_appendf(p, " WHERE rowid=?1"); |
| 196440 | zSql = sqlite3_str_finish(p); |
| @@ -197109,12 +197186,14 @@ | |
| 197109 | if( check.rc==SQLITE_OK ){ |
| 197110 | pStmt = rtreeCheckPrepare(&check, "SELECT * FROM %Q.'%q_rowid'", zDb, zTab); |
| 197111 | if( pStmt ){ |
| 197112 | nAux = sqlite3_column_count(pStmt) - 2; |
| 197113 | sqlite3_finalize(pStmt); |
| 197114 | } |
| 197115 | check.rc = SQLITE_OK; |
| 197116 | } |
| 197117 | |
| 197118 | /* Find number of dimensions in the rtree table. */ |
| 197119 | pStmt = rtreeCheckPrepare(&check, "SELECT * FROM %Q.%Q", zDb, zTab); |
| 197120 | if( pStmt ){ |
| @@ -199185,11 +199264,14 @@ | |
| 199185 | ){ |
| 199186 | RtreeGeomCallback *pGeomCtx; /* Context object for new user-function */ |
| 199187 | |
| 199188 | /* Allocate and populate the context object. */ |
| 199189 | pGeomCtx = (RtreeGeomCallback *)sqlite3_malloc(sizeof(RtreeGeomCallback)); |
| 199190 | if( !pGeomCtx ) return SQLITE_NOMEM; |
| 199191 | pGeomCtx->xGeom = 0; |
| 199192 | pGeomCtx->xQueryFunc = xQueryFunc; |
| 199193 | pGeomCtx->xDestructor = xDestructor; |
| 199194 | pGeomCtx->pContext = pContext; |
| 199195 | return sqlite3_create_function_v2(db, zQueryFunc, -1, SQLITE_ANY, |
| @@ -222468,10 +222550,46 @@ | |
| 222468 | if( p->pStruct!=(Fts5Structure*)pStruct ){ |
| 222469 | return SQLITE_ABORT; |
| 222470 | } |
| 222471 | return SQLITE_OK; |
| 222472 | } |
| 222473 | |
| 222474 | /* |
| 222475 | ** Deserialize and return the structure record currently stored in serialized |
| 222476 | ** form within buffer pData/nData. |
| 222477 | ** |
| @@ -222570,13 +222688,15 @@ | |
| 222570 | *ppOut = pRet; |
| 222571 | return rc; |
| 222572 | } |
| 222573 | |
| 222574 | /* |
| 222575 | ** |
| 222576 | */ |
| 222577 | static void fts5StructureAddLevel(int *pRc, Fts5Structure **ppStruct){ |
| 222578 | if( *pRc==SQLITE_OK ){ |
| 222579 | Fts5Structure *pStruct = *ppStruct; |
| 222580 | int nLevel = pStruct->nLevel; |
| 222581 | sqlite3_int64 nByte = ( |
| 222582 | sizeof(Fts5Structure) + /* Main structure */ |
| @@ -231175,11 +231295,11 @@ | |
| 231175 | int nArg, /* Number of args */ |
| 231176 | sqlite3_value **apUnused /* Function arguments */ |
| 231177 | ){ |
| 231178 | assert( nArg==0 ); |
| 231179 | UNUSED_PARAM2(nArg, apUnused); |
| 231180 | sqlite3_result_text(pCtx, "fts5: 2021-09-06 11:44:19 b3cfe23bec0b95ca673802526704200e2396df715fdded72aa71addd7f47e0e1", -1, SQLITE_TRANSIENT); |
| 231181 | } |
| 231182 | |
| 231183 | /* |
| 231184 | ** Return true if zName is the extension on one of the shadow tables used |
| 231185 | ** by this module. |
| 231186 |
| --- src/sqlite3.c | |
| +++ src/sqlite3.c | |
| @@ -452,11 +452,11 @@ | |
| 452 | ** [sqlite3_libversion_number()], [sqlite3_sourceid()], |
| 453 | ** [sqlite_version()] and [sqlite_source_id()]. |
| 454 | */ |
| 455 | #define SQLITE_VERSION "3.37.0" |
| 456 | #define SQLITE_VERSION_NUMBER 3037000 |
| 457 | #define SQLITE_SOURCE_ID "2021-09-22 14:43:35 d678ecca02698753d1b33e072566112e94ea36d0d3a8f4a24d2b09d131968d88" |
| 458 | |
| 459 | /* |
| 460 | ** CAPI3REF: Run-Time Library Version Numbers |
| 461 | ** KEYWORDS: sqlite3_version sqlite3_sourceid |
| 462 | ** |
| @@ -19842,11 +19842,11 @@ | |
| 19842 | #ifdef SQLITE_TEST |
| 19843 | SQLITE_PRIVATE int sqlite3Utf8To8(unsigned char*); |
| 19844 | #endif |
| 19845 | |
| 19846 | #ifdef SQLITE_OMIT_VIRTUALTABLE |
| 19847 | # define sqlite3VtabClear(D,T) |
| 19848 | # define sqlite3VtabSync(X,Y) SQLITE_OK |
| 19849 | # define sqlite3VtabRollback(X) |
| 19850 | # define sqlite3VtabCommit(X) |
| 19851 | # define sqlite3VtabInSync(db) 0 |
| 19852 | # define sqlite3VtabLock(X) |
| @@ -48856,11 +48856,11 @@ | |
| 48856 | MemStore *p = 0; |
| 48857 | int szName; |
| 48858 | if( (flags & SQLITE_OPEN_MAIN_DB)==0 ){ |
| 48859 | return ORIGVFS(pVfs)->xOpen(ORIGVFS(pVfs), zName, pFd, flags, pOutFlags); |
| 48860 | } |
| 48861 | memset(pFile, 0, sizeof(*pFile)); |
| 48862 | szName = sqlite3Strlen30(zName); |
| 48863 | if( szName>1 && zName[0]=='/' ){ |
| 48864 | int i; |
| 48865 | #ifndef SQLITE_MUTEX_OMIT |
| 48866 | sqlite3_mutex *pVfsMutex = sqlite3MutexAlloc(SQLITE_MUTEX_STATIC_VFS1); |
| @@ -53160,12 +53160,12 @@ | |
| 53160 | |
| 53161 | u16 nExtra; /* Add this many bytes to each in-memory page */ |
| 53162 | i16 nReserve; /* Number of unused bytes at end of each page */ |
| 53163 | u32 vfsFlags; /* Flags for sqlite3_vfs.xOpen() */ |
| 53164 | u32 sectorSize; /* Assumed sector size during rollback */ |
| 53165 | Pgno mxPgno; /* Maximum allowed size of the database */ |
| 53166 | i64 pageSize; /* Number of bytes in a page */ |
| 53167 | i64 journalSizeLimit; /* Size limit for persistent journal files */ |
| 53168 | char *zFilename; /* Name of the database file */ |
| 53169 | char *zJournal; /* Name of the journal file */ |
| 53170 | int (*xBusyHandler)(void*); /* Function to call when busy */ |
| 53171 | void *pBusyHandlerArg; /* Context argument for xBusyHandler */ |
| @@ -59218,12 +59218,12 @@ | |
| 59218 | /* |
| 59219 | ** Return the approximate number of bytes of memory currently |
| 59220 | ** used by the pager and its associated cache. |
| 59221 | */ |
| 59222 | SQLITE_PRIVATE int sqlite3PagerMemUsed(Pager *pPager){ |
| 59223 | int perPageSize = pPager->pageSize + pPager->nExtra |
| 59224 | + (int)(sizeof(PgHdr) + 5*sizeof(void*)); |
| 59225 | return perPageSize*sqlite3PcachePagecount(pPager->pPCache) |
| 59226 | + sqlite3MallocSize(pPager) |
| 59227 | + pPager->pageSize; |
| 59228 | } |
| 59229 | |
| @@ -59413,18 +59413,18 @@ | |
| 59413 | for(ii=nNew; ii<pPager->nSavepoint; ii++){ |
| 59414 | sqlite3BitvecDestroy(pPager->aSavepoint[ii].pInSavepoint); |
| 59415 | } |
| 59416 | pPager->nSavepoint = nNew; |
| 59417 | |
| 59418 | /* Truncate the sub-journal so that it only includes the parts |
| 59419 | ** that are still in use. */ |
| 59420 | if( op==SAVEPOINT_RELEASE ){ |
| 59421 | PagerSavepoint *pRel = &pPager->aSavepoint[nNew]; |
| 59422 | if( pRel->bTruncateOnRelease && isOpen(pPager->sjfd) ){ |
| 59423 | /* Only truncate if it is an in-memory sub-journal. */ |
| 59424 | if( sqlite3JournalIsInMemory(pPager->sjfd) ){ |
| 59425 | i64 sz = (pPager->pageSize+4)*(i64)pRel->iSubRec; |
| 59426 | rc = sqlite3OsTruncate(pPager->sjfd, sz); |
| 59427 | assert( rc==SQLITE_OK ); |
| 59428 | } |
| 59429 | pPager->nSubRec = pRel->iSubRec; |
| 59430 | } |
| @@ -72713,10 +72713,11 @@ | |
| 72713 | nCell -= nTail; |
| 72714 | } |
| 72715 | |
| 72716 | pData = &aData[get2byteNotZero(&aData[hdr+5])]; |
| 72717 | if( pData<pBegin ) goto editpage_fail; |
| 72718 | if( NEVER(pData>pPg->aDataEnd) ) goto editpage_fail; |
| 72719 | |
| 72720 | /* Add cells to the start of the page */ |
| 72721 | if( iNew<iOld ){ |
| 72722 | int nAdd = MIN(nNew,iOld-iNew); |
| 72723 | assert( (iOld-iNew)<nNew || nCell==0 || CORRUPT_DB ); |
| @@ -74118,11 +74119,11 @@ | |
| 74119 | pBt = pPage->pBt; |
| 74120 | ovflPageSize = pBt->usableSize - 4; |
| 74121 | do{ |
| 74122 | rc = btreeGetPage(pBt, ovflPgno, &pPage, 0); |
| 74123 | if( rc ) return rc; |
| 74124 | if( sqlite3PagerPageRefcount(pPage->pDbPage)!=1 || pPage->isInit ){ |
| 74125 | rc = SQLITE_CORRUPT_BKPT; |
| 74126 | }else{ |
| 74127 | if( iOffset+ovflPageSize<(u32)nTotal ){ |
| 74128 | ovflPgno = get4byte(pPage->aData); |
| 74129 | }else{ |
| @@ -94795,10 +94796,15 @@ | |
| 94796 | rc = SQLITE_NOMEM_BKPT; |
| 94797 | }else if( rc==SQLITE_IOERR_CORRUPTFS ){ |
| 94798 | rc = SQLITE_CORRUPT_BKPT; |
| 94799 | } |
| 94800 | assert( rc ); |
| 94801 | #ifdef SQLITE_DEBUG |
| 94802 | if( db->flags & SQLITE_VdbeTrace ){ |
| 94803 | printf("ABORT-due-to-error. rc=%d\n", rc); |
| 94804 | } |
| 94805 | #endif |
| 94806 | if( p->zErrMsg==0 && rc!=SQLITE_IOERR_NOMEM ){ |
| 94807 | sqlite3VdbeError(p, "%s", sqlite3ErrStr(rc)); |
| 94808 | } |
| 94809 | p->rc = rc; |
| 94810 | sqlite3SystemError(db, rc); |
| @@ -105161,10 +105167,11 @@ | |
| 105167 | |
| 105168 | /*********************************************************************** |
| 105169 | ** Test-only SQL functions that are only usable if enabled |
| 105170 | ** via SQLITE_TESTCTRL_INTERNAL_FUNCTIONS |
| 105171 | */ |
| 105172 | #if !defined(SQLITE_UNTESTABLE) |
| 105173 | case INLINEFUNC_expr_compare: { |
| 105174 | /* Compare two expressions using sqlite3ExprCompare() */ |
| 105175 | assert( nFarg==2 ); |
| 105176 | sqlite3VdbeAddOp2(v, OP_Integer, |
| 105177 | sqlite3ExprCompare(0,pFarg->a[0].pExpr, pFarg->a[1].pExpr,-1), |
| @@ -105194,11 +105201,10 @@ | |
| 105201 | sqlite3VdbeAddOp2(v, OP_Null, 0, target); |
| 105202 | } |
| 105203 | break; |
| 105204 | } |
| 105205 | |
| 105206 | case INLINEFUNC_affinity: { |
| 105207 | /* The AFFINITY() function evaluates to a string that describes |
| 105208 | ** the type affinity of the argument. This is used for testing of |
| 105209 | ** the SQLite type logic. |
| 105210 | */ |
| @@ -105208,11 +105214,11 @@ | |
| 105214 | aff = sqlite3ExprAffinity(pFarg->a[0].pExpr); |
| 105215 | sqlite3VdbeLoadString(v, target, |
| 105216 | (aff<=SQLITE_AFF_NONE) ? "none" : azAff[aff-SQLITE_AFF_BLOB]); |
| 105217 | break; |
| 105218 | } |
| 105219 | #endif /* !defined(SQLITE_UNTESTABLE) */ |
| 105220 | } |
| 105221 | return target; |
| 105222 | } |
| 105223 | |
| 105224 | |
| @@ -108172,12 +108178,11 @@ | |
| 108178 | bQuote = sqlite3Isquote(pNew->z[0]); |
| 108179 | sqlite3NestedParse(pParse, |
| 108180 | "UPDATE \"%w\"." DFLT_SCHEMA_TABLE " SET " |
| 108181 | "sql = sqlite_rename_column(sql, type, name, %Q, %Q, %d, %Q, %d, %d) " |
| 108182 | "WHERE name NOT LIKE 'sqliteX_%%' ESCAPE 'X' " |
| 108183 | " AND (type != 'index' OR tbl_name = %Q)", |
| 108184 | zDb, |
| 108185 | zDb, pTab->zName, iCol, zNew, bQuote, iSchema==1, |
| 108186 | pTab->zName |
| 108187 | ); |
| 108188 | |
| @@ -109023,11 +109028,11 @@ | |
| 109028 | rc = (db->mallocFailed ? SQLITE_NOMEM : sParse.rc); |
| 109029 | if( rc==SQLITE_OK ){ |
| 109030 | sqlite3WalkSelect(&sWalker, pSelect); |
| 109031 | } |
| 109032 | if( rc!=SQLITE_OK ) goto renameColumnFunc_done; |
| 109033 | }else if( IsOrdinaryTable(sParse.pNewTable) ){ |
| 109034 | /* A regular table */ |
| 109035 | int bFKOnly = sqlite3_stricmp(zTable, sParse.pNewTable->zName); |
| 109036 | FKey *pFKey; |
| 109037 | sCtx.pTab = sParse.pNewTable; |
| 109038 | if( bFKOnly==0 ){ |
| @@ -120209,13 +120214,13 @@ | |
| 120214 | } |
| 120215 | |
| 120216 | /* |
| 120217 | ** Implementation of the changes() SQL function. |
| 120218 | ** |
| 120219 | ** IMP: R-32760-32347 The changes() SQL function is a wrapper |
| 120220 | ** around the sqlite3_changes64() C/C++ function and hence follows the |
| 120221 | ** same rules for counting changes. |
| 120222 | */ |
| 120223 | static void changes( |
| 120224 | sqlite3_context *context, |
| 120225 | int NotUsed, |
| 120226 | sqlite3_value **NotUsed2 |
| @@ -120234,12 +120239,12 @@ | |
| 120239 | int NotUsed, |
| 120240 | sqlite3_value **NotUsed2 |
| 120241 | ){ |
| 120242 | sqlite3 *db = sqlite3_context_db_handle(context); |
| 120243 | UNUSED_PARAMETER2(NotUsed, NotUsed2); |
| 120244 | /* IMP: R-11217-42568 This function is a wrapper around the |
| 120245 | ** sqlite3_total_changes64() C/C++ interface. */ |
| 120246 | sqlite3_result_int64(context, sqlite3_total_changes64(db)); |
| 120247 | } |
| 120248 | |
| 120249 | /* |
| 120250 | ** A structure defining how to do GLOB-style comparisons. |
| @@ -121761,16 +121766,16 @@ | |
| 121766 | ** |
| 121767 | ** For peak efficiency, put the most frequently used function last. |
| 121768 | */ |
| 121769 | static FuncDef aBuiltinFunc[] = { |
| 121770 | /***** Functions only available with SQLITE_TESTCTRL_INTERNAL_FUNCTIONS *****/ |
| 121771 | #if !defined(SQLITE_UNTESTABLE) |
| 121772 | TEST_FUNC(implies_nonnull_row, 2, INLINEFUNC_implies_nonnull_row, 0), |
| 121773 | TEST_FUNC(expr_compare, 2, INLINEFUNC_expr_compare, 0), |
| 121774 | TEST_FUNC(expr_implies_expr, 2, INLINEFUNC_expr_implies_expr, 0), |
| 121775 | TEST_FUNC(affinity, 1, INLINEFUNC_affinity, 0), |
| 121776 | #endif /* !defined(SQLITE_UNTESTABLE) */ |
| 121777 | /***** Regular functions *****/ |
| 121778 | #ifdef SQLITE_SOUNDEX |
| 121779 | FUNCTION(soundex, 1, 0, 0, soundexFunc ), |
| 121780 | #endif |
| 121781 | #ifndef SQLITE_OMIT_LOAD_EXTENSION |
| @@ -128264,17 +128269,18 @@ | |
| 128269 | #define PragTyp_SECURE_DELETE 33 |
| 128270 | #define PragTyp_SHRINK_MEMORY 34 |
| 128271 | #define PragTyp_SOFT_HEAP_LIMIT 35 |
| 128272 | #define PragTyp_SYNCHRONOUS 36 |
| 128273 | #define PragTyp_TABLE_INFO 37 |
| 128274 | #define PragTyp_TABLE_LIST 38 |
| 128275 | #define PragTyp_TEMP_STORE 39 |
| 128276 | #define PragTyp_TEMP_STORE_DIRECTORY 40 |
| 128277 | #define PragTyp_THREADS 41 |
| 128278 | #define PragTyp_WAL_AUTOCHECKPOINT 42 |
| 128279 | #define PragTyp_WAL_CHECKPOINT 43 |
| 128280 | #define PragTyp_LOCK_STATUS 44 |
| 128281 | #define PragTyp_STATS 45 |
| 128282 | |
| 128283 | /* Property flags associated with various pragma. */ |
| 128284 | #define PragFlg_NeedSchema 0x01 /* Force schema load before running */ |
| 128285 | #define PragFlg_NoColumns 0x02 /* OP_ResultRow called with zero columns */ |
| 128286 | #define PragFlg_NoColumns1 0x04 /* zero columns if RHS argument is present */ |
| @@ -128303,49 +128309,55 @@ | |
| 128309 | /* 11 */ "notnull", |
| 128310 | /* 12 */ "dflt_value", |
| 128311 | /* 13 */ "pk", |
| 128312 | /* 14 */ "hidden", |
| 128313 | /* table_info reuses 8 */ |
| 128314 | /* 15 */ "schema", /* Used by: table_list */ |
| 128315 | /* 16 */ "name", |
| 128316 | /* 17 */ "type", |
| 128317 | /* 18 */ "ncol", |
| 128318 | /* 19 */ "wr", |
| 128319 | /* 20 */ "strict", |
| 128320 | /* 21 */ "seqno", /* Used by: index_xinfo */ |
| 128321 | /* 22 */ "cid", |
| 128322 | /* 23 */ "name", |
| 128323 | /* 24 */ "desc", |
| 128324 | /* 25 */ "coll", |
| 128325 | /* 26 */ "key", |
| 128326 | /* 27 */ "name", /* Used by: function_list */ |
| 128327 | /* 28 */ "builtin", |
| 128328 | /* 29 */ "type", |
| 128329 | /* 30 */ "enc", |
| 128330 | /* 31 */ "narg", |
| 128331 | /* 32 */ "flags", |
| 128332 | /* 33 */ "tbl", /* Used by: stats */ |
| 128333 | /* 34 */ "idx", |
| 128334 | /* 35 */ "wdth", |
| 128335 | /* 36 */ "hght", |
| 128336 | /* 37 */ "flgs", |
| 128337 | /* 38 */ "seq", /* Used by: index_list */ |
| 128338 | /* 39 */ "name", |
| 128339 | /* 40 */ "unique", |
| 128340 | /* 41 */ "origin", |
| 128341 | /* 42 */ "partial", |
| 128342 | /* 43 */ "table", /* Used by: foreign_key_check */ |
| 128343 | /* 44 */ "rowid", |
| 128344 | /* 45 */ "parent", |
| 128345 | /* 46 */ "fkid", |
| 128346 | /* index_info reuses 21 */ |
| 128347 | /* 47 */ "seq", /* Used by: database_list */ |
| 128348 | /* 48 */ "name", |
| 128349 | /* 49 */ "file", |
| 128350 | /* 50 */ "busy", /* Used by: wal_checkpoint */ |
| 128351 | /* 51 */ "log", |
| 128352 | /* 52 */ "checkpointed", |
| 128353 | /* collation_list reuses 38 */ |
| 128354 | /* 53 */ "database", /* Used by: lock_status */ |
| 128355 | /* 54 */ "status", |
| 128356 | /* 55 */ "cache_size", /* Used by: default_cache_size */ |
| 128357 | /* module_list pragma_list reuses 9 */ |
| 128358 | /* 56 */ "timeout", /* Used by: busy_timeout */ |
| 128359 | }; |
| 128360 | |
| 128361 | /* Definitions of all built-in pragmas */ |
| 128362 | typedef struct PragmaName { |
| 128363 | const char *const zName; /* Name of pragma */ |
| @@ -128392,11 +128404,11 @@ | |
| 128404 | #endif |
| 128405 | #endif |
| 128406 | {/* zName: */ "busy_timeout", |
| 128407 | /* ePragTyp: */ PragTyp_BUSY_TIMEOUT, |
| 128408 | /* ePragFlg: */ PragFlg_Result0, |
| 128409 | /* ColNames: */ 56, 1, |
| 128410 | /* iArg: */ 0 }, |
| 128411 | #if !defined(SQLITE_OMIT_PAGER_PRAGMAS) |
| 128412 | {/* zName: */ "cache_size", |
| 128413 | /* ePragTyp: */ PragTyp_CACHE_SIZE, |
| 128414 | /* ePragFlg: */ PragFlg_NeedSchema|PragFlg_Result0|PragFlg_SchemaReq|PragFlg_NoColumns1, |
| @@ -128431,11 +128443,11 @@ | |
| 128443 | #endif |
| 128444 | #if !defined(SQLITE_OMIT_SCHEMA_PRAGMAS) |
| 128445 | {/* zName: */ "collation_list", |
| 128446 | /* ePragTyp: */ PragTyp_COLLATION_LIST, |
| 128447 | /* ePragFlg: */ PragFlg_Result0, |
| 128448 | /* ColNames: */ 38, 2, |
| 128449 | /* iArg: */ 0 }, |
| 128450 | #endif |
| 128451 | #if !defined(SQLITE_OMIT_COMPILEOPTION_DIAGS) |
| 128452 | {/* zName: */ "compile_options", |
| 128453 | /* ePragTyp: */ PragTyp_COMPILE_OPTIONS, |
| @@ -128466,18 +128478,18 @@ | |
| 128478 | #endif |
| 128479 | #if !defined(SQLITE_OMIT_SCHEMA_PRAGMAS) |
| 128480 | {/* zName: */ "database_list", |
| 128481 | /* ePragTyp: */ PragTyp_DATABASE_LIST, |
| 128482 | /* ePragFlg: */ PragFlg_NeedSchema|PragFlg_Result0, |
| 128483 | /* ColNames: */ 47, 3, |
| 128484 | /* iArg: */ 0 }, |
| 128485 | #endif |
| 128486 | #if !defined(SQLITE_OMIT_PAGER_PRAGMAS) && !defined(SQLITE_OMIT_DEPRECATED) |
| 128487 | {/* zName: */ "default_cache_size", |
| 128488 | /* ePragTyp: */ PragTyp_DEFAULT_CACHE_SIZE, |
| 128489 | /* ePragFlg: */ PragFlg_NeedSchema|PragFlg_Result0|PragFlg_SchemaReq|PragFlg_NoColumns1, |
| 128490 | /* ColNames: */ 55, 1, |
| 128491 | /* iArg: */ 0 }, |
| 128492 | #endif |
| 128493 | #if !defined(SQLITE_OMIT_FLAG_PRAGMAS) |
| 128494 | #if !defined(SQLITE_OMIT_FOREIGN_KEY) && !defined(SQLITE_OMIT_TRIGGER) |
| 128495 | {/* zName: */ "defer_foreign_keys", |
| @@ -128503,11 +128515,11 @@ | |
| 128515 | #endif |
| 128516 | #if !defined(SQLITE_OMIT_FOREIGN_KEY) && !defined(SQLITE_OMIT_TRIGGER) |
| 128517 | {/* zName: */ "foreign_key_check", |
| 128518 | /* ePragTyp: */ PragTyp_FOREIGN_KEY_CHECK, |
| 128519 | /* ePragFlg: */ PragFlg_NeedSchema|PragFlg_Result0|PragFlg_Result1|PragFlg_SchemaOpt, |
| 128520 | /* ColNames: */ 43, 4, |
| 128521 | /* iArg: */ 0 }, |
| 128522 | #endif |
| 128523 | #if !defined(SQLITE_OMIT_FOREIGN_KEY) |
| 128524 | {/* zName: */ "foreign_key_list", |
| 128525 | /* ePragTyp: */ PragTyp_FOREIGN_KEY_LIST, |
| @@ -128546,11 +128558,11 @@ | |
| 128558 | #if !defined(SQLITE_OMIT_SCHEMA_PRAGMAS) |
| 128559 | #if !defined(SQLITE_OMIT_INTROSPECTION_PRAGMAS) |
| 128560 | {/* zName: */ "function_list", |
| 128561 | /* ePragTyp: */ PragTyp_FUNCTION_LIST, |
| 128562 | /* ePragFlg: */ PragFlg_Result0, |
| 128563 | /* ColNames: */ 27, 6, |
| 128564 | /* iArg: */ 0 }, |
| 128565 | #endif |
| 128566 | #endif |
| 128567 | {/* zName: */ "hard_heap_limit", |
| 128568 | /* ePragTyp: */ PragTyp_HARD_HEAP_LIMIT, |
| @@ -128575,21 +128587,21 @@ | |
| 128587 | #endif |
| 128588 | #if !defined(SQLITE_OMIT_SCHEMA_PRAGMAS) |
| 128589 | {/* zName: */ "index_info", |
| 128590 | /* ePragTyp: */ PragTyp_INDEX_INFO, |
| 128591 | /* ePragFlg: */ PragFlg_NeedSchema|PragFlg_Result1|PragFlg_SchemaOpt, |
| 128592 | /* ColNames: */ 21, 3, |
| 128593 | /* iArg: */ 0 }, |
| 128594 | {/* zName: */ "index_list", |
| 128595 | /* ePragTyp: */ PragTyp_INDEX_LIST, |
| 128596 | /* ePragFlg: */ PragFlg_NeedSchema|PragFlg_Result1|PragFlg_SchemaOpt, |
| 128597 | /* ColNames: */ 38, 5, |
| 128598 | /* iArg: */ 0 }, |
| 128599 | {/* zName: */ "index_xinfo", |
| 128600 | /* ePragTyp: */ PragTyp_INDEX_INFO, |
| 128601 | /* ePragFlg: */ PragFlg_NeedSchema|PragFlg_Result1|PragFlg_SchemaOpt, |
| 128602 | /* ColNames: */ 21, 6, |
| 128603 | /* iArg: */ 1 }, |
| 128604 | #endif |
| 128605 | #if !defined(SQLITE_OMIT_INTEGRITY_CHECK) |
| 128606 | {/* zName: */ "integrity_check", |
| 128607 | /* ePragTyp: */ PragTyp_INTEGRITY_CHECK, |
| @@ -128625,11 +128637,11 @@ | |
| 128637 | #endif |
| 128638 | #if defined(SQLITE_DEBUG) || defined(SQLITE_TEST) |
| 128639 | {/* zName: */ "lock_status", |
| 128640 | /* ePragTyp: */ PragTyp_LOCK_STATUS, |
| 128641 | /* ePragFlg: */ PragFlg_Result0, |
| 128642 | /* ColNames: */ 53, 2, |
| 128643 | /* iArg: */ 0 }, |
| 128644 | #endif |
| 128645 | #if !defined(SQLITE_OMIT_PAGER_PRAGMAS) |
| 128646 | {/* zName: */ "locking_mode", |
| 128647 | /* ePragTyp: */ PragTyp_LOCKING_MODE, |
| @@ -128764,11 +128776,11 @@ | |
| 128776 | #endif |
| 128777 | #if !defined(SQLITE_OMIT_SCHEMA_PRAGMAS) && defined(SQLITE_DEBUG) |
| 128778 | {/* zName: */ "stats", |
| 128779 | /* ePragTyp: */ PragTyp_STATS, |
| 128780 | /* ePragFlg: */ PragFlg_NeedSchema|PragFlg_Result0|PragFlg_SchemaReq, |
| 128781 | /* ColNames: */ 33, 5, |
| 128782 | /* iArg: */ 0 }, |
| 128783 | #endif |
| 128784 | #if !defined(SQLITE_OMIT_PAGER_PRAGMAS) |
| 128785 | {/* zName: */ "synchronous", |
| 128786 | /* ePragTyp: */ PragTyp_SYNCHRONOUS, |
| @@ -128780,10 +128792,15 @@ | |
| 128792 | {/* zName: */ "table_info", |
| 128793 | /* ePragTyp: */ PragTyp_TABLE_INFO, |
| 128794 | /* ePragFlg: */ PragFlg_NeedSchema|PragFlg_Result1|PragFlg_SchemaOpt, |
| 128795 | /* ColNames: */ 8, 6, |
| 128796 | /* iArg: */ 0 }, |
| 128797 | {/* zName: */ "table_list", |
| 128798 | /* ePragTyp: */ PragTyp_TABLE_LIST, |
| 128799 | /* ePragFlg: */ PragFlg_NeedSchema|PragFlg_Result1, |
| 128800 | /* ColNames: */ 15, 6, |
| 128801 | /* iArg: */ 1 }, |
| 128802 | {/* zName: */ "table_xinfo", |
| 128803 | /* ePragTyp: */ PragTyp_TABLE_INFO, |
| 128804 | /* ePragFlg: */ PragFlg_NeedSchema|PragFlg_Result1|PragFlg_SchemaOpt, |
| 128805 | /* ColNames: */ 8, 7, |
| 128806 | /* iArg: */ 1 }, |
| @@ -128855,11 +128872,11 @@ | |
| 128872 | /* ColNames: */ 0, 0, |
| 128873 | /* iArg: */ 0 }, |
| 128874 | {/* zName: */ "wal_checkpoint", |
| 128875 | /* ePragTyp: */ PragTyp_WAL_CHECKPOINT, |
| 128876 | /* ePragFlg: */ PragFlg_NeedSchema, |
| 128877 | /* ColNames: */ 50, 3, |
| 128878 | /* iArg: */ 0 }, |
| 128879 | #endif |
| 128880 | #if !defined(SQLITE_OMIT_FLAG_PRAGMAS) |
| 128881 | {/* zName: */ "writable_schema", |
| 128882 | /* ePragTyp: */ PragTyp_FLAG, |
| @@ -128866,11 +128883,11 @@ | |
| 128883 | /* ePragFlg: */ PragFlg_Result0|PragFlg_NoColumns1, |
| 128884 | /* ColNames: */ 0, 0, |
| 128885 | /* iArg: */ SQLITE_WriteSchema|SQLITE_NoSchemaError }, |
| 128886 | #endif |
| 128887 | }; |
| 128888 | /* Number of pragmas: 68 on by default, 78 total. */ |
| 128889 | |
| 128890 | /************** End of pragma.h **********************************************/ |
| 128891 | /************** Continuing where we left off in pragma.c *********************/ |
| 128892 | |
| 128893 | /* |
| @@ -130035,10 +130052,58 @@ | |
| 130052 | } |
| 130053 | } |
| 130054 | } |
| 130055 | break; |
| 130056 | |
| 130057 | /* |
| 130058 | ** PRAGMA table_list |
| 130059 | ** |
| 130060 | ** Return a single row for each table, virtual table, or view in the |
| 130061 | ** entire schema. |
| 130062 | ** |
| 130063 | ** schema: Name of attached database hold this table |
| 130064 | ** name: Name of the table itself |
| 130065 | ** type: "table", "view", "virtual", "shadow" |
| 130066 | ** ncol: Number of columns |
| 130067 | ** wr: True for a WITHOUT ROWID table |
| 130068 | ** strict: True for a STRICT table |
| 130069 | */ |
| 130070 | case PragTyp_TABLE_LIST: { |
| 130071 | int ii; |
| 130072 | pParse->nMem = 6; |
| 130073 | sqlite3CodeVerifyNamedSchema(pParse, zDb); |
| 130074 | for(ii=0; ii<db->nDb; ii++){ |
| 130075 | HashElem *k; |
| 130076 | Hash *pHash; |
| 130077 | if( zDb && sqlite3_stricmp(zDb, db->aDb[ii].zDbSName)!=0 ) continue; |
| 130078 | pHash = &db->aDb[ii].pSchema->tblHash; |
| 130079 | for(k=sqliteHashFirst(pHash); k; k=sqliteHashNext(k) ){ |
| 130080 | Table *pTab = sqliteHashData(k); |
| 130081 | const char *zType; |
| 130082 | if( zRight && sqlite3_stricmp(zRight, pTab->zName)!=0 ) continue; |
| 130083 | if( IsView(pTab) ){ |
| 130084 | zType = "view"; |
| 130085 | }else if( IsVirtual(pTab) ){ |
| 130086 | zType = "virtual"; |
| 130087 | }else if( pTab->tabFlags & TF_Shadow ){ |
| 130088 | zType = "shadow"; |
| 130089 | }else{ |
| 130090 | zType = "table"; |
| 130091 | } |
| 130092 | sqlite3VdbeMultiLoad(v, 1, "sssiii", |
| 130093 | db->aDb[ii].zDbSName, |
| 130094 | pTab->zName, |
| 130095 | zType, |
| 130096 | pTab->nCol, |
| 130097 | (pTab->tabFlags & TF_WithoutRowid)!=0, |
| 130098 | (pTab->tabFlags & TF_Strict)!=0 |
| 130099 | ); |
| 130100 | } |
| 130101 | } |
| 130102 | } |
| 130103 | break; |
| 130104 | |
| 130105 | #ifdef SQLITE_DEBUG |
| 130106 | case PragTyp_STATS: { |
| 130107 | Index *pIdx; |
| 130108 | HashElem *i; |
| 130109 | pParse->nMem = 5; |
| @@ -130544,11 +130609,13 @@ | |
| 130609 | }else{ |
| 130610 | integrityCheckResultRow(v); |
| 130611 | } |
| 130612 | sqlite3VdbeJumpHere(v, jmp2); |
| 130613 | } |
| 130614 | if( (pTab->tabFlags & TF_Strict)!=0 |
| 130615 | && pCol->eCType!=COLTYPE_ANY |
| 130616 | ){ |
| 130617 | jmp2 = sqlite3VdbeAddOp3(v, OP_IsNullOrType, 3, 0, |
| 130618 | sqlite3StdTypeMap[pCol->eCType-1]); |
| 130619 | VdbeCoverage(v); |
| 130620 | zErr = sqlite3MPrintf(db, "non-%s value in %s.%s", |
| 130621 | sqlite3StdType[pCol->eCType-1], |
| @@ -132836,10 +132903,13 @@ | |
| 132903 | |
| 132904 | pE1 = sqlite3CreateColumnExpr(db, pSrc, iLeft, iColLeft); |
| 132905 | pE2 = sqlite3CreateColumnExpr(db, pSrc, iRight, iColRight); |
| 132906 | |
| 132907 | pEq = sqlite3PExpr(pParse, TK_EQ, pE1, pE2); |
| 132908 | assert( pE2!=0 || pEq==0 ); /* Due to db->mallocFailed test |
| 132909 | ** in sqlite3DbMallocRawNN() called from |
| 132910 | ** sqlite3PExpr(). */ |
| 132911 | if( pEq && isOuterJoin ){ |
| 132912 | ExprSetProperty(pEq, EP_FromJoin); |
| 132913 | assert( !ExprHasProperty(pEq, EP_TokenOnly|EP_Reduced) ); |
| 132914 | ExprSetVVAProperty(pEq, EP_NoReduce); |
| 132915 | pEq->iRightJoinTable = pE2->iTable; |
| @@ -148365,11 +148435,11 @@ | |
| 148435 | ** 2019-06-14 https://sqlite.org/src/info/ce8717f0885af975 |
| 148436 | ** 2019-09-03 https://sqlite.org/src/info/0f0428096f17252a |
| 148437 | */ |
| 148438 | if( pLeft->op!=TK_COLUMN |
| 148439 | || sqlite3ExprAffinity(pLeft)!=SQLITE_AFF_TEXT |
| 148440 | || (pLeft->y.pTab && IsVirtual(pLeft->y.pTab)) /* Might be numeric */ |
| 148441 | ){ |
| 148442 | int isNum; |
| 148443 | double rDummy; |
| 148444 | isNum = sqlite3AtoF(zNew, &rDummy, iTo, SQLITE_UTF8); |
| 148445 | if( isNum<=0 ){ |
| @@ -156490,10 +156560,13 @@ | |
| 156560 | ); |
| 156561 | SELECTTRACE(1,pParse,pSub, |
| 156562 | ("New window-function subquery in FROM clause of (%u/%p)\n", |
| 156563 | p->selId, p)); |
| 156564 | p->pSrc = sqlite3SrcListAppend(pParse, 0, 0, 0); |
| 156565 | assert( pSub!=0 || p->pSrc==0 ); /* Due to db->mallocFailed test inside |
| 156566 | ** of sqlite3DbMallocRawNN() called from |
| 156567 | ** sqlite3SrcListAppend() */ |
| 156568 | if( p->pSrc ){ |
| 156569 | Table *pTab2; |
| 156570 | p->pSrc->a[0].pSelect = pSub; |
| 156571 | sqlite3SrcListAssignCursors(pParse, p->pSrc); |
| 156572 | pSub->selFlags |= SF_Expanded|SF_OrderByReqd; |
| @@ -192908,11 +192981,15 @@ | |
| 192981 | #else |
| 192982 | /* #include "sqlite3.h" */ |
| 192983 | #endif |
| 192984 | SQLITE_PRIVATE int sqlite3GetToken(const unsigned char*,int*); /* In the SQLite core */ |
| 192985 | |
| 192986 | /* |
| 192987 | ** If building separately, we will need some setup that is normally |
| 192988 | ** found in sqliteInt.h |
| 192989 | */ |
| 192990 | #if !defined(SQLITE_AMALGAMATION) |
| 192991 | #include "sqlite3rtree.h" |
| 192992 | typedef sqlite3_int64 i64; |
| 192993 | typedef sqlite3_uint64 u64; |
| 192994 | typedef unsigned char u8; |
| 192995 | typedef unsigned short u16; |
| @@ -192921,11 +192998,21 @@ | |
| 192998 | # define NDEBUG 1 |
| 192999 | #endif |
| 193000 | #if defined(NDEBUG) && defined(SQLITE_DEBUG) |
| 193001 | # undef NDEBUG |
| 193002 | #endif |
| 193003 | #if defined(SQLITE_COVERAGE_TEST) || defined(SQLITE_MUTATION_TEST) |
| 193004 | # define ALWAYS(X) (1) |
| 193005 | # define NEVER(X) (0) |
| 193006 | #elif !defined(NDEBUG) |
| 193007 | # define ALWAYS(X) ((X)?1:(assert(0),0)) |
| 193008 | # define NEVER(X) ((X)?(assert(0),1):0) |
| 193009 | #else |
| 193010 | # define ALWAYS(X) (X) |
| 193011 | # define NEVER(X) (X) |
| 193012 | #endif |
| 193013 | #endif /* !defined(SQLITE_AMALGAMATION) */ |
| 193014 | |
| 193015 | /* #include <string.h> */ |
| 193016 | /* #include <stdio.h> */ |
| 193017 | /* #include <assert.h> */ |
| 193018 | /* #include <stdlib.h> */ |
| @@ -192979,11 +193066,13 @@ | |
| 193066 | u8 nDim2; /* Twice the number of dimensions */ |
| 193067 | u8 eCoordType; /* RTREE_COORD_REAL32 or RTREE_COORD_INT32 */ |
| 193068 | u8 nBytesPerCell; /* Bytes consumed per cell */ |
| 193069 | u8 inWrTrans; /* True if inside write transaction */ |
| 193070 | u8 nAux; /* # of auxiliary columns in %_rowid */ |
| 193071 | #ifdef SQLITE_ENABLE_GEOPOLY |
| 193072 | u8 nAuxNotNull; /* Number of initial not-null aux columns */ |
| 193073 | #endif |
| 193074 | #ifdef SQLITE_DEBUG |
| 193075 | u8 bCorrupt; /* Shadow table corruption detected */ |
| 193076 | #endif |
| 193077 | int iDepth; /* Current depth of the r-tree structure */ |
| 193078 | char *zDb; /* Name of database containing r-tree table */ |
| @@ -193510,22 +193599,10 @@ | |
| 193599 | pRtree->pNodeBlob = 0; |
| 193600 | sqlite3_blob_close(pBlob); |
| 193601 | } |
| 193602 | } |
| 193603 | |
| 193604 | /* |
| 193605 | ** Obtain a reference to an r-tree node. |
| 193606 | */ |
| 193607 | static int nodeAcquire( |
| 193608 | Rtree *pRtree, /* R-tree structure */ |
| @@ -193538,18 +193615,11 @@ | |
| 193615 | |
| 193616 | /* Check if the requested node is already in the hash table. If so, |
| 193617 | ** increase its reference count and return it. |
| 193618 | */ |
| 193619 | if( (pNode = nodeHashLookup(pRtree, iNode))!=0 ){ |
| 193620 | if( pParent && pParent!=pNode->pParent ){ |
| 193621 | RTREE_IS_CORRUPT(pRtree); |
| 193622 | return SQLITE_CORRUPT_VTAB; |
| 193623 | } |
| 193624 | pNode->nRef++; |
| 193625 | *ppNode = pNode; |
| @@ -193603,11 +193673,11 @@ | |
| 193673 | ** of the r-tree structure. A height of zero means all data is stored on |
| 193674 | ** the root node. A height of one means the children of the root node |
| 193675 | ** are the leaves, and so on. If the depth as specified on the root node |
| 193676 | ** is greater than RTREE_MAX_DEPTH, the r-tree structure must be corrupt. |
| 193677 | */ |
| 193678 | if( rc==SQLITE_OK && pNode && iNode==1 ){ |
| 193679 | pRtree->iDepth = readInt16(pNode->zData); |
| 193680 | if( pRtree->iDepth>RTREE_MAX_DEPTH ){ |
| 193681 | rc = SQLITE_CORRUPT_VTAB; |
| 193682 | RTREE_IS_CORRUPT(pRtree); |
| 193683 | } |
| @@ -194209,15 +194279,16 @@ | |
| 194279 | ** Return the index of the cell containing a pointer to node pNode |
| 194280 | ** in its parent. If pNode is the root node, return -1. |
| 194281 | */ |
| 194282 | static int nodeParentIndex(Rtree *pRtree, RtreeNode *pNode, int *piIndex){ |
| 194283 | RtreeNode *pParent = pNode->pParent; |
| 194284 | if( ALWAYS(pParent) ){ |
| 194285 | return nodeRowidIndex(pRtree, pParent, pNode->iNode, piIndex); |
| 194286 | }else{ |
| 194287 | *piIndex = -1; |
| 194288 | return SQLITE_OK; |
| 194289 | } |
| 194290 | } |
| 194291 | |
| 194292 | /* |
| 194293 | ** Compare two search points. Return negative, zero, or positive if the first |
| 194294 | ** is less than, equal to, or greater than the second. |
| @@ -194336,11 +194407,12 @@ | |
| 194407 | if( pCur->bPoint ){ |
| 194408 | int ii; |
| 194409 | pNew = rtreeEnqueue(pCur, rScore, iLevel); |
| 194410 | if( pNew==0 ) return 0; |
| 194411 | ii = (int)(pNew - pCur->aPoint) + 1; |
| 194412 | assert( ii==1 ); |
| 194413 | if( ALWAYS(ii<RTREE_CACHE_SZ) ){ |
| 194414 | assert( pCur->aNode[ii]==0 ); |
| 194415 | pCur->aNode[ii] = pCur->aNode[0]; |
| 194416 | }else{ |
| 194417 | nodeRelease(RTREE_OF_CURSOR(pCur), pCur->aNode[0]); |
| 194418 | } |
| @@ -194397,11 +194469,11 @@ | |
| 194469 | p->aNode[i] = 0; |
| 194470 | } |
| 194471 | if( p->bPoint ){ |
| 194472 | p->anQueue[p->sPoint.iLevel]--; |
| 194473 | p->bPoint = 0; |
| 194474 | }else if( ALWAYS(p->nPoint) ){ |
| 194475 | p->anQueue[p->aPoint[0].iLevel]--; |
| 194476 | n = --p->nPoint; |
| 194477 | p->aPoint[0] = p->aPoint[n]; |
| 194478 | if( n<RTREE_CACHE_SZ-1 ){ |
| 194479 | p->aNode[1] = p->aNode[n+1]; |
| @@ -194538,11 +194610,11 @@ | |
| 194610 | static int rtreeRowid(sqlite3_vtab_cursor *pVtabCursor, sqlite_int64 *pRowid){ |
| 194611 | RtreeCursor *pCsr = (RtreeCursor *)pVtabCursor; |
| 194612 | RtreeSearchPoint *p = rtreeSearchPointFirst(pCsr); |
| 194613 | int rc = SQLITE_OK; |
| 194614 | RtreeNode *pNode = rtreeNodeOfFirstSearchPoint(pCsr, &rc); |
| 194615 | if( rc==SQLITE_OK && ALWAYS(p) ){ |
| 194616 | *pRowid = nodeGetRowid(RTREE_OF_CURSOR(pCsr), pNode, p->iCell); |
| 194617 | } |
| 194618 | return rc; |
| 194619 | } |
| 194620 | |
| @@ -194556,11 +194628,11 @@ | |
| 194628 | RtreeCoord c; |
| 194629 | int rc = SQLITE_OK; |
| 194630 | RtreeNode *pNode = rtreeNodeOfFirstSearchPoint(pCsr, &rc); |
| 194631 | |
| 194632 | if( rc ) return rc; |
| 194633 | if( NEVER(p==0) ) return SQLITE_OK; |
| 194634 | if( i==0 ){ |
| 194635 | sqlite3_result_int64(ctx, nodeGetRowid(pRtree, pNode, p->iCell)); |
| 194636 | }else if( i<=pRtree->nDim2 ){ |
| 194637 | nodeGetCoord(pRtree, pNode, p->iCell, i-1, &c); |
| 194638 | #ifndef SQLITE_RTREE_INT_ONLY |
| @@ -194755,12 +194827,15 @@ | |
| 194827 | } |
| 194828 | } |
| 194829 | } |
| 194830 | if( rc==SQLITE_OK ){ |
| 194831 | RtreeSearchPoint *pNew; |
| 194832 | assert( pCsr->bPoint==0 ); /* Due to the resetCursor() call above */ |
| 194833 | pNew = rtreeSearchPointNew(pCsr, RTREE_ZERO, (u8)(pRtree->iDepth+1)); |
| 194834 | if( NEVER(pNew==0) ){ /* Because pCsr->bPoint was FALSE */ |
| 194835 | return SQLITE_NOMEM; |
| 194836 | } |
| 194837 | pNew->id = 1; |
| 194838 | pNew->iCell = 0; |
| 194839 | pNew->eWithin = PARTLY_WITHIN; |
| 194840 | assert( pCsr->bPoint==1 ); |
| 194841 | pCsr->aNode[0] = pRoot; |
| @@ -194833,11 +194908,11 @@ | |
| 194908 | assert( pIdxInfo->idxStr==0 ); |
| 194909 | for(ii=0; ii<pIdxInfo->nConstraint && iIdx<(int)(sizeof(zIdxStr)-1); ii++){ |
| 194910 | struct sqlite3_index_constraint *p = &pIdxInfo->aConstraint[ii]; |
| 194911 | |
| 194912 | if( bMatch==0 && p->usable |
| 194913 | && p->iColumn<=0 && p->op==SQLITE_INDEX_CONSTRAINT_EQ |
| 194914 | ){ |
| 194915 | /* We have an equality constraint on the rowid. Use strategy 1. */ |
| 194916 | int jj; |
| 194917 | for(jj=0; jj<ii; jj++){ |
| 194918 | pIdxInfo->aConstraintUsage[jj].argvIndex = 0; |
| @@ -195086,16 +195161,23 @@ | |
| 195161 | RtreeNode *pNode, /* Adjust ancestry of this node. */ |
| 195162 | RtreeCell *pCell /* This cell was just inserted */ |
| 195163 | ){ |
| 195164 | RtreeNode *p = pNode; |
| 195165 | int cnt = 0; |
| 195166 | int rc; |
| 195167 | while( p->pParent ){ |
| 195168 | RtreeNode *pParent = p->pParent; |
| 195169 | RtreeCell cell; |
| 195170 | int iCell; |
| 195171 | |
| 195172 | cnt++; |
| 195173 | if( NEVER(cnt>100) ){ |
| 195174 | RTREE_IS_CORRUPT(pRtree); |
| 195175 | return SQLITE_CORRUPT_VTAB; |
| 195176 | } |
| 195177 | rc = nodeParentIndex(pRtree, p, &iCell); |
| 195178 | if( NEVER(rc!=SQLITE_OK) ){ |
| 195179 | RTREE_IS_CORRUPT(pRtree); |
| 195180 | return SQLITE_CORRUPT_VTAB; |
| 195181 | } |
| 195182 | |
| 195183 | nodeGetCell(pRtree, pParent, iCell, &cell); |
| @@ -195475,15 +195557,16 @@ | |
| 195557 | } |
| 195558 | }else{ |
| 195559 | RtreeNode *pParent = pLeft->pParent; |
| 195560 | int iCell; |
| 195561 | rc = nodeParentIndex(pRtree, pLeft, &iCell); |
| 195562 | if( ALWAYS(rc==SQLITE_OK) ){ |
| 195563 | nodeOverwriteCell(pRtree, pParent, &leftbbox, iCell); |
| 195564 | rc = AdjustTree(pRtree, pParent, &leftbbox); |
| 195565 | assert( rc==SQLITE_OK ); |
| 195566 | } |
| 195567 | if( NEVER(rc!=SQLITE_OK) ){ |
| 195568 | goto splitnode_out; |
| 195569 | } |
| 195570 | } |
| 195571 | if( (rc = rtreeInsertCell(pRtree, pRight->pParent, &rightbbox, iHeight+1)) ){ |
| 195572 | goto splitnode_out; |
| @@ -195554,11 +195637,11 @@ | |
| 195637 | ** want to do this as it leads to a memory leak when trying to delete |
| 195638 | ** the referenced counted node structures. |
| 195639 | */ |
| 195640 | iNode = sqlite3_column_int64(pRtree->pReadParent, 0); |
| 195641 | for(pTest=pLeaf; pTest && pTest->iNode!=iNode; pTest=pTest->pParent); |
| 195642 | if( pTest==0 ){ |
| 195643 | rc2 = nodeAcquire(pRtree, iNode, 0, &pChild->pParent); |
| 195644 | } |
| 195645 | } |
| 195646 | rc = sqlite3_reset(pRtree->pReadParent); |
| 195647 | if( rc==SQLITE_OK ) rc = rc2; |
| @@ -195585,10 +195668,11 @@ | |
| 195668 | rc = nodeParentIndex(pRtree, pNode, &iCell); |
| 195669 | if( rc==SQLITE_OK ){ |
| 195670 | pParent = pNode->pParent; |
| 195671 | pNode->pParent = 0; |
| 195672 | rc = deleteCell(pRtree, pParent, iCell, iHeight+1); |
| 195673 | testcase( rc!=SQLITE_OK ); |
| 195674 | } |
| 195675 | rc2 = nodeRelease(pRtree, pParent); |
| 195676 | if( rc==SQLITE_OK ){ |
| 195677 | rc = rc2; |
| 195678 | } |
| @@ -195807,11 +195891,11 @@ | |
| 195891 | pRtree->iReinsertHeight = iHeight; |
| 195892 | rc = Reinsert(pRtree, pNode, pCell, iHeight); |
| 195893 | } |
| 195894 | }else{ |
| 195895 | rc = AdjustTree(pRtree, pNode, pCell); |
| 195896 | if( ALWAYS(rc==SQLITE_OK) ){ |
| 195897 | if( iHeight==0 ){ |
| 195898 | rc = rowidWrite(pRtree, pCell->iRowid, pNode->iNode); |
| 195899 | }else{ |
| 195900 | rc = parentWrite(pRtree, pCell->iRowid, pNode->iNode); |
| 195901 | } |
| @@ -195913,11 +195997,11 @@ | |
| 195997 | */ |
| 195998 | if( rc==SQLITE_OK && pRtree->iDepth>0 && NCELL(pRoot)==1 ){ |
| 195999 | int rc2; |
| 196000 | RtreeNode *pChild = 0; |
| 196001 | i64 iChild = nodeGetRowid(pRtree, pRoot, 0); |
| 196002 | rc = nodeAcquire(pRtree, iChild, pRoot, &pChild); /* tag-20210916a */ |
| 196003 | if( rc==SQLITE_OK ){ |
| 196004 | rc = removeNode(pRtree, pChild, pRtree->iDepth-1); |
| 196005 | } |
| 196006 | rc2 = nodeRelease(pRtree, pChild); |
| 196007 | if( rc==SQLITE_OK ) rc = rc2; |
| @@ -196248,11 +196332,11 @@ | |
| 196332 | static int rtreeQueryStat1(sqlite3 *db, Rtree *pRtree){ |
| 196333 | const char *zFmt = "SELECT stat FROM %Q.sqlite_stat1 WHERE tbl = '%q_rowid'"; |
| 196334 | char *zSql; |
| 196335 | sqlite3_stmt *p; |
| 196336 | int rc; |
| 196337 | i64 nRow = RTREE_MIN_ROWEST; |
| 196338 | |
| 196339 | rc = sqlite3_table_column_metadata( |
| 196340 | db, pRtree->zDb, "sqlite_stat1",0,0,0,0,0,0 |
| 196341 | ); |
| 196342 | if( rc!=SQLITE_OK ){ |
| @@ -196265,24 +196349,14 @@ | |
| 196349 | }else{ |
| 196350 | rc = sqlite3_prepare_v2(db, zSql, -1, &p, 0); |
| 196351 | if( rc==SQLITE_OK ){ |
| 196352 | if( sqlite3_step(p)==SQLITE_ROW ) nRow = sqlite3_column_int64(p, 0); |
| 196353 | rc = sqlite3_finalize(p); |
| 196354 | } |
| 196355 | sqlite3_free(zSql); |
| 196356 | } |
| 196357 | pRtree->nRowEst = MAX(nRow, RTREE_MIN_ROWEST); |
| 196358 | return rc; |
| 196359 | } |
| 196360 | |
| 196361 | |
| 196362 | /* |
| @@ -196428,13 +196502,16 @@ | |
| 196502 | int ii; |
| 196503 | char *zSql; |
| 196504 | sqlite3_str_appendf(p, "UPDATE \"%w\".\"%w_rowid\"SET ", zDb, zPrefix); |
| 196505 | for(ii=0; ii<pRtree->nAux; ii++){ |
| 196506 | if( ii ) sqlite3_str_append(p, ",", 1); |
| 196507 | #ifdef SQLITE_ENABLE_GEOPOLY |
| 196508 | if( ii<pRtree->nAuxNotNull ){ |
| 196509 | sqlite3_str_appendf(p,"a%d=coalesce(?%d,a%d)",ii,ii+2,ii); |
| 196510 | }else |
| 196511 | #endif |
| 196512 | { |
| 196513 | sqlite3_str_appendf(p,"a%d=?%d",ii,ii+2); |
| 196514 | } |
| 196515 | } |
| 196516 | sqlite3_str_appendf(p, " WHERE rowid=?1"); |
| 196517 | zSql = sqlite3_str_finish(p); |
| @@ -197109,12 +197186,14 @@ | |
| 197186 | if( check.rc==SQLITE_OK ){ |
| 197187 | pStmt = rtreeCheckPrepare(&check, "SELECT * FROM %Q.'%q_rowid'", zDb, zTab); |
| 197188 | if( pStmt ){ |
| 197189 | nAux = sqlite3_column_count(pStmt) - 2; |
| 197190 | sqlite3_finalize(pStmt); |
| 197191 | }else |
| 197192 | if( check.rc!=SQLITE_NOMEM ){ |
| 197193 | check.rc = SQLITE_OK; |
| 197194 | } |
| 197195 | } |
| 197196 | |
| 197197 | /* Find number of dimensions in the rtree table. */ |
| 197198 | pStmt = rtreeCheckPrepare(&check, "SELECT * FROM %Q.%Q", zDb, zTab); |
| 197199 | if( pStmt ){ |
| @@ -199185,11 +199264,14 @@ | |
| 199264 | ){ |
| 199265 | RtreeGeomCallback *pGeomCtx; /* Context object for new user-function */ |
| 199266 | |
| 199267 | /* Allocate and populate the context object. */ |
| 199268 | pGeomCtx = (RtreeGeomCallback *)sqlite3_malloc(sizeof(RtreeGeomCallback)); |
| 199269 | if( !pGeomCtx ){ |
| 199270 | if( xDestructor ) xDestructor(pContext); |
| 199271 | return SQLITE_NOMEM; |
| 199272 | } |
| 199273 | pGeomCtx->xGeom = 0; |
| 199274 | pGeomCtx->xQueryFunc = xQueryFunc; |
| 199275 | pGeomCtx->xDestructor = xDestructor; |
| 199276 | pGeomCtx->pContext = pContext; |
| 199277 | return sqlite3_create_function_v2(db, zQueryFunc, -1, SQLITE_ANY, |
| @@ -222468,10 +222550,46 @@ | |
| 222550 | if( p->pStruct!=(Fts5Structure*)pStruct ){ |
| 222551 | return SQLITE_ABORT; |
| 222552 | } |
| 222553 | return SQLITE_OK; |
| 222554 | } |
| 222555 | |
| 222556 | /* |
| 222557 | ** Ensure that structure object (*pp) is writable. |
| 222558 | ** |
| 222559 | ** This function is a no-op if (*pRc) is not SQLITE_OK when it is called. If |
| 222560 | ** an error occurs, (*pRc) is set to an SQLite error code before returning. |
| 222561 | */ |
| 222562 | static void fts5StructureMakeWritable(int *pRc, Fts5Structure **pp){ |
| 222563 | Fts5Structure *p = *pp; |
| 222564 | if( *pRc==SQLITE_OK && p->nRef>1 ){ |
| 222565 | int nByte = sizeof(Fts5Structure)+(p->nLevel-1)*sizeof(Fts5StructureLevel); |
| 222566 | Fts5Structure *pNew; |
| 222567 | pNew = (Fts5Structure*)sqlite3Fts5MallocZero(pRc, nByte); |
| 222568 | if( pNew ){ |
| 222569 | int i; |
| 222570 | memcpy(pNew, p, nByte); |
| 222571 | for(i=0; i<p->nLevel; i++) pNew->aLevel[i].aSeg = 0; |
| 222572 | for(i=0; i<p->nLevel; i++){ |
| 222573 | Fts5StructureLevel *pLvl = &pNew->aLevel[i]; |
| 222574 | nByte = sizeof(Fts5StructureSegment) * pNew->aLevel[i].nSeg; |
| 222575 | pLvl->aSeg = (Fts5StructureSegment*)sqlite3Fts5MallocZero(pRc, nByte); |
| 222576 | if( pLvl->aSeg==0 ){ |
| 222577 | for(i=0; i<p->nLevel; i++){ |
| 222578 | sqlite3_free(pNew->aLevel[i].aSeg); |
| 222579 | } |
| 222580 | sqlite3_free(pNew); |
| 222581 | return; |
| 222582 | } |
| 222583 | memcpy(pLvl->aSeg, p->aLevel[i].aSeg, nByte); |
| 222584 | } |
| 222585 | p->nRef--; |
| 222586 | pNew->nRef = 1; |
| 222587 | } |
| 222588 | *pp = pNew; |
| 222589 | } |
| 222590 | } |
| 222591 | |
| 222592 | /* |
| 222593 | ** Deserialize and return the structure record currently stored in serialized |
| 222594 | ** form within buffer pData/nData. |
| 222595 | ** |
| @@ -222570,13 +222688,15 @@ | |
| 222688 | *ppOut = pRet; |
| 222689 | return rc; |
| 222690 | } |
| 222691 | |
| 222692 | /* |
| 222693 | ** Add a level to the Fts5Structure.aLevel[] array of structure object |
| 222694 | ** (*ppStruct). |
| 222695 | */ |
| 222696 | static void fts5StructureAddLevel(int *pRc, Fts5Structure **ppStruct){ |
| 222697 | fts5StructureMakeWritable(pRc, ppStruct); |
| 222698 | if( *pRc==SQLITE_OK ){ |
| 222699 | Fts5Structure *pStruct = *ppStruct; |
| 222700 | int nLevel = pStruct->nLevel; |
| 222701 | sqlite3_int64 nByte = ( |
| 222702 | sizeof(Fts5Structure) + /* Main structure */ |
| @@ -231175,11 +231295,11 @@ | |
| 231295 | int nArg, /* Number of args */ |
| 231296 | sqlite3_value **apUnused /* Function arguments */ |
| 231297 | ){ |
| 231298 | assert( nArg==0 ); |
| 231299 | UNUSED_PARAM2(nArg, apUnused); |
| 231300 | sqlite3_result_text(pCtx, "fts5: 2021-09-22 14:43:35 d678ecca02698753d1b33e072566112e94ea36d0d3a8f4a24d2b09d131968d88", -1, SQLITE_TRANSIENT); |
| 231301 | } |
| 231302 | |
| 231303 | /* |
| 231304 | ** Return true if zName is the extension on one of the shadow tables used |
| 231305 | ** by this module. |
| 231306 |
+1
-1
| --- src/sqlite3.h | ||
| +++ src/sqlite3.h | ||
| @@ -146,11 +146,11 @@ | ||
| 146 | 146 | ** [sqlite3_libversion_number()], [sqlite3_sourceid()], |
| 147 | 147 | ** [sqlite_version()] and [sqlite_source_id()]. |
| 148 | 148 | */ |
| 149 | 149 | #define SQLITE_VERSION "3.37.0" |
| 150 | 150 | #define SQLITE_VERSION_NUMBER 3037000 |
| 151 | -#define SQLITE_SOURCE_ID "2021-09-06 11:44:19 b3cfe23bec0b95ca673802526704200e2396df715fdded72aa71addd7f47e0e1" | |
| 151 | +#define SQLITE_SOURCE_ID "2021-09-22 14:43:35 d678ecca02698753d1b33e072566112e94ea36d0d3a8f4a24d2b09d131968d88" | |
| 152 | 152 | |
| 153 | 153 | /* |
| 154 | 154 | ** CAPI3REF: Run-Time Library Version Numbers |
| 155 | 155 | ** KEYWORDS: sqlite3_version sqlite3_sourceid |
| 156 | 156 | ** |
| 157 | 157 |
| --- src/sqlite3.h | |
| +++ src/sqlite3.h | |
| @@ -146,11 +146,11 @@ | |
| 146 | ** [sqlite3_libversion_number()], [sqlite3_sourceid()], |
| 147 | ** [sqlite_version()] and [sqlite_source_id()]. |
| 148 | */ |
| 149 | #define SQLITE_VERSION "3.37.0" |
| 150 | #define SQLITE_VERSION_NUMBER 3037000 |
| 151 | #define SQLITE_SOURCE_ID "2021-09-06 11:44:19 b3cfe23bec0b95ca673802526704200e2396df715fdded72aa71addd7f47e0e1" |
| 152 | |
| 153 | /* |
| 154 | ** CAPI3REF: Run-Time Library Version Numbers |
| 155 | ** KEYWORDS: sqlite3_version sqlite3_sourceid |
| 156 | ** |
| 157 |
| --- src/sqlite3.h | |
| +++ src/sqlite3.h | |
| @@ -146,11 +146,11 @@ | |
| 146 | ** [sqlite3_libversion_number()], [sqlite3_sourceid()], |
| 147 | ** [sqlite_version()] and [sqlite_source_id()]. |
| 148 | */ |
| 149 | #define SQLITE_VERSION "3.37.0" |
| 150 | #define SQLITE_VERSION_NUMBER 3037000 |
| 151 | #define SQLITE_SOURCE_ID "2021-09-22 14:43:35 d678ecca02698753d1b33e072566112e94ea36d0d3a8f4a24d2b09d131968d88" |
| 152 | |
| 153 | /* |
| 154 | ** CAPI3REF: Run-Time Library Version Numbers |
| 155 | ** KEYWORDS: sqlite3_version sqlite3_sourceid |
| 156 | ** |
| 157 |
+131
-52
| --- src/style.chat.css | ||
| +++ src/style.chat.css | ||
| @@ -9,14 +9,18 @@ | ||
| 9 | 9 | border: none; |
| 10 | 10 | display: flex; |
| 11 | 11 | flex-direction: column; |
| 12 | 12 | border: none; |
| 13 | 13 | align-items: flex-start; |
| 14 | +} | |
| 15 | +body.chat .message-widget:last-of-type { | |
| 16 | + /* Latest message: reduce bottom gap */ | |
| 17 | + margin-bottom: 0.1em; | |
| 14 | 18 | } |
| 15 | 19 | body.chat.my-messages-right .message-widget.mine { |
| 16 | 20 | /* Right-aligns a user's own chat messages, similar to how |
| 17 | - most mobile messaging apps do it. */ | |
| 21 | + most/some mobile messaging apps do it. */ | |
| 18 | 22 | align-items: flex-end; |
| 19 | 23 | } |
| 20 | 24 | body.chat.my-messages-right .message-widget.notification { |
| 21 | 25 | /* Center-aligns a system-level notification message. */ |
| 22 | 26 | align-items: center; |
| @@ -77,10 +81,12 @@ | ||
| 77 | 81 | display: flex; |
| 78 | 82 | flex-direction: column; |
| 79 | 83 | align-items: stretch; |
| 80 | 84 | padding: 0.25em; |
| 81 | 85 | margin-top: 0.25em; |
| 86 | + border: 1px outset; | |
| 87 | + border-radius: 0.5em; | |
| 82 | 88 | } |
| 83 | 89 | /* Full message timestamps. */ |
| 84 | 90 | body.chat .chat-message-popup > span { white-space: nowrap; } |
| 85 | 91 | /* Container for the message deletion buttons. */ |
| 86 | 92 | body.chat .chat-message-popup > .toolbar { |
| @@ -132,53 +138,14 @@ | ||
| 132 | 138 | flex-direction: column; |
| 133 | 139 | align-items: stretch; |
| 134 | 140 | padding: 0.25em; |
| 135 | 141 | z-index: 200; |
| 136 | 142 | } |
| 137 | -body.chat .chat-settings-popup > span { | |
| 138 | - vertical-align: middle; | |
| 139 | -} | |
| 140 | -body.chat .chat-settings-popup > span.menu-entry{ | |
| 141 | - white-space: nowrap; | |
| 142 | - cursor: pointer; | |
| 143 | - border: 1px solid; | |
| 144 | - border-radius: 0.25em; | |
| 145 | - padding: 0.25em 0.5em; | |
| 146 | - display: flex; | |
| 147 | - flex-direction: row; | |
| 148 | - align-items: center; | |
| 149 | - justify-content: space-between; | |
| 150 | -} | |
| 151 | -body.chat .chat-settings-popup > span.menu-entry:hover { | |
| 152 | -} | |
| 153 | -body.chat .chat-settings-popup > span.menu-entry > .help-buttonlet { | |
| 154 | - vertical-align: middle; | |
| 155 | -} | |
| 156 | -body.chat .chat-settings-popup > span.menu-entry > span.button { | |
| 157 | - margin: 0.25em 0.2em; | |
| 158 | - padding: 0.25em; | |
| 159 | - flex: 1 1 auto/*eliminates dead no-click zones on the right*/; | |
| 160 | -} | |
| 161 | -body.chat .chat-settings-popup > span.menu-entry > input[type=checkbox] { | |
| 162 | - cursor: inherit; | |
| 163 | -} | |
| 164 | -body.chat .chat-settings-popup > select.menu-entry { | |
| 165 | - flex: 1 1 auto; | |
| 166 | - padding: 0; | |
| 167 | - cursor: pointer; | |
| 168 | - min-height: 2.5em; | |
| 169 | - border-radius: 0.25em; | |
| 170 | -} | |
| 171 | -body.chat .chat-settings-popup > select.menu-entry > option { | |
| 172 | - /*Recall that many browsers don't allow styling of OPTION | |
| 173 | - elements, or allow only very limited styling.*/ | |
| 174 | -} | |
| 175 | 143 | |
| 176 | 144 | /** Container for the list of /chat messages. */ |
| 177 | 145 | body.chat #chat-messages-wrapper { |
| 178 | 146 | overflow: auto; |
| 179 | - flex: 2 1 auto; | |
| 180 | 147 | padding: 0 0.25em; |
| 181 | 148 | } |
| 182 | 149 | body.chat #chat-messages-wrapper.loading > * { |
| 183 | 150 | /* An attempt at reducing flicker when loading lots of messages. */ |
| 184 | 151 | visibility: hidden; |
| @@ -199,16 +166,14 @@ | ||
| 199 | 166 | } |
| 200 | 167 | /* Wrapper for /chat user input controls */ |
| 201 | 168 | body.chat #chat-input-area { |
| 202 | 169 | display: flex; |
| 203 | 170 | flex-direction: column; |
| 204 | - padding: 0.5em 0 0 0; | |
| 205 | - border-bottom: none; | |
| 206 | - border-top: 1px solid black; | |
| 207 | - margin: 0.5em 0; | |
| 171 | + padding: 0; | |
| 172 | + margin: 0; | |
| 208 | 173 | position: initial /*sticky currently disabled due to scrolling-related issues*/; |
| 209 | - bottom: 0; | |
| 174 | + /*bottom: 0;*/ | |
| 210 | 175 | } |
| 211 | 176 | body.chat:not(.chat-only-mode) #chat-input-area{ |
| 212 | 177 | /* Safari user reports that 2em is necessary to keep the file selection |
| 213 | 178 | widget from overlapping the page footer, whereas a margin of 0 is fine |
| 214 | 179 | for FF/Chrome (and 2em is a *huge* waste of space for those). */ |
| @@ -234,10 +199,11 @@ | ||
| 234 | 199 | body.chat #chat-input-line.single-line #chat-edit-buttons { |
| 235 | 200 | flex-direction: row; |
| 236 | 201 | } |
| 237 | 202 | body.chat #chat-edit-buttons > * { |
| 238 | 203 | flex: 1 1 auto; |
| 204 | + padding: initial/*some skins mess this up for buttons*/; | |
| 239 | 205 | } |
| 240 | 206 | body.chat #chat-input-line:not(.single-line) #chat-edit-buttons > * { |
| 241 | 207 | max-width: 4em; |
| 242 | 208 | margin: 0.25em; |
| 243 | 209 | } |
| @@ -265,17 +231,17 @@ | ||
| 265 | 231 | body.chat #chat-input-file-area { |
| 266 | 232 | display: flex; |
| 267 | 233 | flex-direction: row; |
| 268 | 234 | align-items: center; |
| 269 | 235 | flex-wrap: wrap; |
| 270 | - margin-bottom: 0.25em 0 0 0 /* avoid nudging input area */; | |
| 236 | + margin: 0.25em 0 0 0 /* avoid nudging input area */; | |
| 271 | 237 | } |
| 272 | 238 | body.chat #chat-input-file-area > .file-selection-wrapper { |
| 273 | 239 | align-self: flex-start; |
| 274 | 240 | margin-right: 0.5em; |
| 275 | 241 | flex: 0 1 auto; |
| 276 | - padding: 0.25em 0.25em 0.25em 0; | |
| 242 | + padding: 0.25em 0.5em; | |
| 277 | 243 | white-space: nowrap; |
| 278 | 244 | } |
| 279 | 245 | body.chat #chat-input-file-area .file-selection-wrapper > * { |
| 280 | 246 | vertical-align: middle; |
| 281 | 247 | margin: 0; |
| @@ -303,39 +269,48 @@ | ||
| 303 | 269 | |
| 304 | 270 | body.chat #chat-drop-details img { |
| 305 | 271 | max-width: 45%; |
| 306 | 272 | max-height: 45%; |
| 307 | 273 | } |
| 308 | - | |
| 274 | +body.chat .chat-view { | |
| 275 | + flex: 20 1 auto | |
| 276 | + /*ensure that these grow more than the non-.chat-view elements*/; | |
| 277 | + margin-bottom: 0.2em; | |
| 278 | +} | |
| 309 | 279 | body.chat #chat-config, |
| 310 | 280 | body.chat #chat-preview { |
| 311 | 281 | /* /chat configuration widget */ |
| 312 | 282 | display: flex; |
| 313 | 283 | flex-direction: column; |
| 314 | 284 | overflow: auto; |
| 315 | - flex: 2 1 auto; | |
| 316 | 285 | padding: 0; |
| 317 | 286 | margin: 0; |
| 318 | 287 | align-items: stretch; |
| 319 | 288 | min-height: 6em; |
| 320 | 289 | } |
| 321 | 290 | body.chat #chat-config #chat-config-options { |
| 322 | 291 | /* /chat config options go here */ |
| 323 | 292 | flex: 1 1 auto; |
| 324 | 293 | display: flex; |
| 325 | - flex-direction: column; | |
| 294 | + flex-direction: column-reverse; | |
| 326 | 295 | overflow: auto; |
| 327 | 296 | } |
| 328 | 297 | body.chat #chat-config #chat-config-options .menu-entry { |
| 329 | 298 | display: flex; |
| 330 | 299 | align-items: center; |
| 331 | 300 | flex-direction: row; |
| 332 | - flex-wrap: wrap; | |
| 301 | + flex-wrap: nowrap; | |
| 333 | 302 | padding: 1em; |
| 334 | 303 | } |
| 335 | -body.chat #chat-config #chat-config-options .menu-entry > *:first-child { | |
| 304 | +body.chat #chat-config #chat-config-options .menu-entry > label { | |
| 305 | + cursor: pointer; | |
| 306 | +} | |
| 307 | +body.chat #chat-config #chat-config-options .menu-entry > input:first-child { | |
| 336 | 308 | margin-right: 1em; |
| 309 | +} | |
| 310 | +body.chat #chat-config #chat-config-options .menu-entry > label:first-child { | |
| 311 | + margin-right: 0.5em; | |
| 337 | 312 | } |
| 338 | 313 | body.chat #chat-preview #chat-preview-content { |
| 339 | 314 | overflow: auto; |
| 340 | 315 | flex: 1 1 auto; |
| 341 | 316 | padding: 0.5em; |
| @@ -354,5 +329,109 @@ | ||
| 354 | 329 | body.chat #chat-preview #chat-preview-buttons > button { |
| 355 | 330 | padding: 0.5em; |
| 356 | 331 | flex: 0 1 auto; |
| 357 | 332 | margin: 0.25em 0; |
| 358 | 333 | } |
| 334 | + | |
| 335 | +body.chat #chat-user-list-wrapper { | |
| 336 | + /* Safari can't do fieldsets right, so we emulate one. */ | |
| 337 | + border-radius: 0.5em; | |
| 338 | + margin: 1em 0 0.2em 0; | |
| 339 | + padding: 0 0.5em; | |
| 340 | + border-style: inset; | |
| 341 | + border-width: 0 1px 1px 1px/*else collides with the LEGEND*/; | |
| 342 | +} | |
| 343 | +body.chat #chat-user-list-wrapper.collapsed { | |
| 344 | + padding: 0; | |
| 345 | +} | |
| 346 | +body.chat #chat-user-list-wrapper > .legend { | |
| 347 | + font-weight: initial; | |
| 348 | + padding: 0 0.5em 0 0.5em; | |
| 349 | + position: relative; | |
| 350 | + top: -1.75ex/* place it like a fieldset legend */; | |
| 351 | + cursor: pointer; | |
| 352 | +} | |
| 353 | +body.chat #chat-user-list-wrapper > .legend > * { | |
| 354 | + vertical-align: middle; | |
| 355 | +} | |
| 356 | +body.chat #chat-user-list-wrapper > .legend > *:nth-child(2){ | |
| 357 | + /* Title label */ | |
| 358 | + opacity: 0.6; | |
| 359 | + font-size: 0.8em; | |
| 360 | +} | |
| 361 | +body.chat #chat-user-list-wrapper.collapsed > .legend > *:nth-child(2)::after { | |
| 362 | + content: " (tap to toggle)"; | |
| 363 | +} | |
| 364 | +body.chat #chat-user-list-wrapper .help-buttonlet { | |
| 365 | + margin: 0; | |
| 366 | +} | |
| 367 | +body.chat #chat-user-list-wrapper.collapsed #chat-user-list { | |
| 368 | + position: absolute !important; | |
| 369 | + opacity: 0 !important; | |
| 370 | + pointer-events: none !important; | |
| 371 | + display: none !important; | |
| 372 | +} | |
| 373 | +body.chat #chat-user-list { | |
| 374 | + margin-top: -1.25ex; | |
| 375 | + display: flex; | |
| 376 | + flex-direction: row; | |
| 377 | + flex-wrap: wrap; | |
| 378 | + align-items: center; | |
| 379 | +} | |
| 380 | +body.chat #chat-user-list .chat-user { | |
| 381 | + margin: 0.2em; | |
| 382 | + padding: 0.1em 0.5em 0.2em 0.5em; | |
| 383 | + border-radius: 0.5em; | |
| 384 | + cursor: pointer; | |
| 385 | + text-align: center; | |
| 386 | + white-space: pre; | |
| 387 | +} | |
| 388 | +body.chat #chat-user-list .timestamp { | |
| 389 | + font-size: 85%; | |
| 390 | + font-family: monospace; | |
| 391 | +} | |
| 392 | +body.chat #chat-user-list:not(.timestamps) .timestamp { | |
| 393 | + display: none; | |
| 394 | +} | |
| 395 | +body.chat #chat-user-list .chat-user.selected { | |
| 396 | + font-weight: bold; | |
| 397 | + text-decoration: underline; | |
| 398 | +} | |
| 399 | + | |
| 400 | +body.chat .anim-rotate-360 { | |
| 401 | + animation: rotate-360 750ms linear; | |
| 402 | +} | |
| 403 | +@keyframes rotate-360 { | |
| 404 | + from { transform: rotate(0deg); } | |
| 405 | + to { transform: rotate(360deg); } | |
| 406 | +} | |
| 407 | +body.chat .anim-flip-h { | |
| 408 | + animation: flip-h 750ms linear; | |
| 409 | +} | |
| 410 | +@keyframes flip-h{ | |
| 411 | + from { transform: rotateY(0deg); } | |
| 412 | + to { transform: rotateY(360deg); } | |
| 413 | +} | |
| 414 | +body.chat .anim-flip-v { | |
| 415 | + animation: flip-v 750ms linear; | |
| 416 | +} | |
| 417 | +@keyframes flip-v{ | |
| 418 | + from { transform: rotateX(0deg); } | |
| 419 | + to { transform: rotateX(360deg); } | |
| 420 | +} | |
| 421 | +body.chat .anim-fade-in { | |
| 422 | + animation: fade-in 750ms linear; | |
| 423 | +} | |
| 424 | +body.chat .anim-fade-in-fast { | |
| 425 | + animation: fade-in 350ms linear; | |
| 426 | +} | |
| 427 | +@keyframes fade-in { | |
| 428 | + from { opacity: 0; } | |
| 429 | + to { opacity: 1; } | |
| 430 | +} | |
| 431 | +body.chat .anim-fade-out-fast { | |
| 432 | + animation: fade-out 250ms linear; | |
| 433 | +} | |
| 434 | +@keyframes fade-out { | |
| 435 | + from { opacity: 1; } | |
| 436 | + to { opacity: 0; } | |
| 437 | +} | |
| 359 | 438 |
| --- src/style.chat.css | |
| +++ src/style.chat.css | |
| @@ -9,14 +9,18 @@ | |
| 9 | border: none; |
| 10 | display: flex; |
| 11 | flex-direction: column; |
| 12 | border: none; |
| 13 | align-items: flex-start; |
| 14 | } |
| 15 | body.chat.my-messages-right .message-widget.mine { |
| 16 | /* Right-aligns a user's own chat messages, similar to how |
| 17 | most mobile messaging apps do it. */ |
| 18 | align-items: flex-end; |
| 19 | } |
| 20 | body.chat.my-messages-right .message-widget.notification { |
| 21 | /* Center-aligns a system-level notification message. */ |
| 22 | align-items: center; |
| @@ -77,10 +81,12 @@ | |
| 77 | display: flex; |
| 78 | flex-direction: column; |
| 79 | align-items: stretch; |
| 80 | padding: 0.25em; |
| 81 | margin-top: 0.25em; |
| 82 | } |
| 83 | /* Full message timestamps. */ |
| 84 | body.chat .chat-message-popup > span { white-space: nowrap; } |
| 85 | /* Container for the message deletion buttons. */ |
| 86 | body.chat .chat-message-popup > .toolbar { |
| @@ -132,53 +138,14 @@ | |
| 132 | flex-direction: column; |
| 133 | align-items: stretch; |
| 134 | padding: 0.25em; |
| 135 | z-index: 200; |
| 136 | } |
| 137 | body.chat .chat-settings-popup > span { |
| 138 | vertical-align: middle; |
| 139 | } |
| 140 | body.chat .chat-settings-popup > span.menu-entry{ |
| 141 | white-space: nowrap; |
| 142 | cursor: pointer; |
| 143 | border: 1px solid; |
| 144 | border-radius: 0.25em; |
| 145 | padding: 0.25em 0.5em; |
| 146 | display: flex; |
| 147 | flex-direction: row; |
| 148 | align-items: center; |
| 149 | justify-content: space-between; |
| 150 | } |
| 151 | body.chat .chat-settings-popup > span.menu-entry:hover { |
| 152 | } |
| 153 | body.chat .chat-settings-popup > span.menu-entry > .help-buttonlet { |
| 154 | vertical-align: middle; |
| 155 | } |
| 156 | body.chat .chat-settings-popup > span.menu-entry > span.button { |
| 157 | margin: 0.25em 0.2em; |
| 158 | padding: 0.25em; |
| 159 | flex: 1 1 auto/*eliminates dead no-click zones on the right*/; |
| 160 | } |
| 161 | body.chat .chat-settings-popup > span.menu-entry > input[type=checkbox] { |
| 162 | cursor: inherit; |
| 163 | } |
| 164 | body.chat .chat-settings-popup > select.menu-entry { |
| 165 | flex: 1 1 auto; |
| 166 | padding: 0; |
| 167 | cursor: pointer; |
| 168 | min-height: 2.5em; |
| 169 | border-radius: 0.25em; |
| 170 | } |
| 171 | body.chat .chat-settings-popup > select.menu-entry > option { |
| 172 | /*Recall that many browsers don't allow styling of OPTION |
| 173 | elements, or allow only very limited styling.*/ |
| 174 | } |
| 175 | |
| 176 | /** Container for the list of /chat messages. */ |
| 177 | body.chat #chat-messages-wrapper { |
| 178 | overflow: auto; |
| 179 | flex: 2 1 auto; |
| 180 | padding: 0 0.25em; |
| 181 | } |
| 182 | body.chat #chat-messages-wrapper.loading > * { |
| 183 | /* An attempt at reducing flicker when loading lots of messages. */ |
| 184 | visibility: hidden; |
| @@ -199,16 +166,14 @@ | |
| 199 | } |
| 200 | /* Wrapper for /chat user input controls */ |
| 201 | body.chat #chat-input-area { |
| 202 | display: flex; |
| 203 | flex-direction: column; |
| 204 | padding: 0.5em 0 0 0; |
| 205 | border-bottom: none; |
| 206 | border-top: 1px solid black; |
| 207 | margin: 0.5em 0; |
| 208 | position: initial /*sticky currently disabled due to scrolling-related issues*/; |
| 209 | bottom: 0; |
| 210 | } |
| 211 | body.chat:not(.chat-only-mode) #chat-input-area{ |
| 212 | /* Safari user reports that 2em is necessary to keep the file selection |
| 213 | widget from overlapping the page footer, whereas a margin of 0 is fine |
| 214 | for FF/Chrome (and 2em is a *huge* waste of space for those). */ |
| @@ -234,10 +199,11 @@ | |
| 234 | body.chat #chat-input-line.single-line #chat-edit-buttons { |
| 235 | flex-direction: row; |
| 236 | } |
| 237 | body.chat #chat-edit-buttons > * { |
| 238 | flex: 1 1 auto; |
| 239 | } |
| 240 | body.chat #chat-input-line:not(.single-line) #chat-edit-buttons > * { |
| 241 | max-width: 4em; |
| 242 | margin: 0.25em; |
| 243 | } |
| @@ -265,17 +231,17 @@ | |
| 265 | body.chat #chat-input-file-area { |
| 266 | display: flex; |
| 267 | flex-direction: row; |
| 268 | align-items: center; |
| 269 | flex-wrap: wrap; |
| 270 | margin-bottom: 0.25em 0 0 0 /* avoid nudging input area */; |
| 271 | } |
| 272 | body.chat #chat-input-file-area > .file-selection-wrapper { |
| 273 | align-self: flex-start; |
| 274 | margin-right: 0.5em; |
| 275 | flex: 0 1 auto; |
| 276 | padding: 0.25em 0.25em 0.25em 0; |
| 277 | white-space: nowrap; |
| 278 | } |
| 279 | body.chat #chat-input-file-area .file-selection-wrapper > * { |
| 280 | vertical-align: middle; |
| 281 | margin: 0; |
| @@ -303,39 +269,48 @@ | |
| 303 | |
| 304 | body.chat #chat-drop-details img { |
| 305 | max-width: 45%; |
| 306 | max-height: 45%; |
| 307 | } |
| 308 | |
| 309 | body.chat #chat-config, |
| 310 | body.chat #chat-preview { |
| 311 | /* /chat configuration widget */ |
| 312 | display: flex; |
| 313 | flex-direction: column; |
| 314 | overflow: auto; |
| 315 | flex: 2 1 auto; |
| 316 | padding: 0; |
| 317 | margin: 0; |
| 318 | align-items: stretch; |
| 319 | min-height: 6em; |
| 320 | } |
| 321 | body.chat #chat-config #chat-config-options { |
| 322 | /* /chat config options go here */ |
| 323 | flex: 1 1 auto; |
| 324 | display: flex; |
| 325 | flex-direction: column; |
| 326 | overflow: auto; |
| 327 | } |
| 328 | body.chat #chat-config #chat-config-options .menu-entry { |
| 329 | display: flex; |
| 330 | align-items: center; |
| 331 | flex-direction: row; |
| 332 | flex-wrap: wrap; |
| 333 | padding: 1em; |
| 334 | } |
| 335 | body.chat #chat-config #chat-config-options .menu-entry > *:first-child { |
| 336 | margin-right: 1em; |
| 337 | } |
| 338 | body.chat #chat-preview #chat-preview-content { |
| 339 | overflow: auto; |
| 340 | flex: 1 1 auto; |
| 341 | padding: 0.5em; |
| @@ -354,5 +329,109 @@ | |
| 354 | body.chat #chat-preview #chat-preview-buttons > button { |
| 355 | padding: 0.5em; |
| 356 | flex: 0 1 auto; |
| 357 | margin: 0.25em 0; |
| 358 | } |
| 359 |
| --- src/style.chat.css | |
| +++ src/style.chat.css | |
| @@ -9,14 +9,18 @@ | |
| 9 | border: none; |
| 10 | display: flex; |
| 11 | flex-direction: column; |
| 12 | border: none; |
| 13 | align-items: flex-start; |
| 14 | } |
| 15 | body.chat .message-widget:last-of-type { |
| 16 | /* Latest message: reduce bottom gap */ |
| 17 | margin-bottom: 0.1em; |
| 18 | } |
| 19 | body.chat.my-messages-right .message-widget.mine { |
| 20 | /* Right-aligns a user's own chat messages, similar to how |
| 21 | most/some mobile messaging apps do it. */ |
| 22 | align-items: flex-end; |
| 23 | } |
| 24 | body.chat.my-messages-right .message-widget.notification { |
| 25 | /* Center-aligns a system-level notification message. */ |
| 26 | align-items: center; |
| @@ -77,10 +81,12 @@ | |
| 81 | display: flex; |
| 82 | flex-direction: column; |
| 83 | align-items: stretch; |
| 84 | padding: 0.25em; |
| 85 | margin-top: 0.25em; |
| 86 | border: 1px outset; |
| 87 | border-radius: 0.5em; |
| 88 | } |
| 89 | /* Full message timestamps. */ |
| 90 | body.chat .chat-message-popup > span { white-space: nowrap; } |
| 91 | /* Container for the message deletion buttons. */ |
| 92 | body.chat .chat-message-popup > .toolbar { |
| @@ -132,53 +138,14 @@ | |
| 138 | flex-direction: column; |
| 139 | align-items: stretch; |
| 140 | padding: 0.25em; |
| 141 | z-index: 200; |
| 142 | } |
| 143 | |
| 144 | /** Container for the list of /chat messages. */ |
| 145 | body.chat #chat-messages-wrapper { |
| 146 | overflow: auto; |
| 147 | padding: 0 0.25em; |
| 148 | } |
| 149 | body.chat #chat-messages-wrapper.loading > * { |
| 150 | /* An attempt at reducing flicker when loading lots of messages. */ |
| 151 | visibility: hidden; |
| @@ -199,16 +166,14 @@ | |
| 166 | } |
| 167 | /* Wrapper for /chat user input controls */ |
| 168 | body.chat #chat-input-area { |
| 169 | display: flex; |
| 170 | flex-direction: column; |
| 171 | padding: 0; |
| 172 | margin: 0; |
| 173 | position: initial /*sticky currently disabled due to scrolling-related issues*/; |
| 174 | /*bottom: 0;*/ |
| 175 | } |
| 176 | body.chat:not(.chat-only-mode) #chat-input-area{ |
| 177 | /* Safari user reports that 2em is necessary to keep the file selection |
| 178 | widget from overlapping the page footer, whereas a margin of 0 is fine |
| 179 | for FF/Chrome (and 2em is a *huge* waste of space for those). */ |
| @@ -234,10 +199,11 @@ | |
| 199 | body.chat #chat-input-line.single-line #chat-edit-buttons { |
| 200 | flex-direction: row; |
| 201 | } |
| 202 | body.chat #chat-edit-buttons > * { |
| 203 | flex: 1 1 auto; |
| 204 | padding: initial/*some skins mess this up for buttons*/; |
| 205 | } |
| 206 | body.chat #chat-input-line:not(.single-line) #chat-edit-buttons > * { |
| 207 | max-width: 4em; |
| 208 | margin: 0.25em; |
| 209 | } |
| @@ -265,17 +231,17 @@ | |
| 231 | body.chat #chat-input-file-area { |
| 232 | display: flex; |
| 233 | flex-direction: row; |
| 234 | align-items: center; |
| 235 | flex-wrap: wrap; |
| 236 | margin: 0.25em 0 0 0 /* avoid nudging input area */; |
| 237 | } |
| 238 | body.chat #chat-input-file-area > .file-selection-wrapper { |
| 239 | align-self: flex-start; |
| 240 | margin-right: 0.5em; |
| 241 | flex: 0 1 auto; |
| 242 | padding: 0.25em 0.5em; |
| 243 | white-space: nowrap; |
| 244 | } |
| 245 | body.chat #chat-input-file-area .file-selection-wrapper > * { |
| 246 | vertical-align: middle; |
| 247 | margin: 0; |
| @@ -303,39 +269,48 @@ | |
| 269 | |
| 270 | body.chat #chat-drop-details img { |
| 271 | max-width: 45%; |
| 272 | max-height: 45%; |
| 273 | } |
| 274 | body.chat .chat-view { |
| 275 | flex: 20 1 auto |
| 276 | /*ensure that these grow more than the non-.chat-view elements*/; |
| 277 | margin-bottom: 0.2em; |
| 278 | } |
| 279 | body.chat #chat-config, |
| 280 | body.chat #chat-preview { |
| 281 | /* /chat configuration widget */ |
| 282 | display: flex; |
| 283 | flex-direction: column; |
| 284 | overflow: auto; |
| 285 | padding: 0; |
| 286 | margin: 0; |
| 287 | align-items: stretch; |
| 288 | min-height: 6em; |
| 289 | } |
| 290 | body.chat #chat-config #chat-config-options { |
| 291 | /* /chat config options go here */ |
| 292 | flex: 1 1 auto; |
| 293 | display: flex; |
| 294 | flex-direction: column-reverse; |
| 295 | overflow: auto; |
| 296 | } |
| 297 | body.chat #chat-config #chat-config-options .menu-entry { |
| 298 | display: flex; |
| 299 | align-items: center; |
| 300 | flex-direction: row; |
| 301 | flex-wrap: nowrap; |
| 302 | padding: 1em; |
| 303 | } |
| 304 | body.chat #chat-config #chat-config-options .menu-entry > label { |
| 305 | cursor: pointer; |
| 306 | } |
| 307 | body.chat #chat-config #chat-config-options .menu-entry > input:first-child { |
| 308 | margin-right: 1em; |
| 309 | } |
| 310 | body.chat #chat-config #chat-config-options .menu-entry > label:first-child { |
| 311 | margin-right: 0.5em; |
| 312 | } |
| 313 | body.chat #chat-preview #chat-preview-content { |
| 314 | overflow: auto; |
| 315 | flex: 1 1 auto; |
| 316 | padding: 0.5em; |
| @@ -354,5 +329,109 @@ | |
| 329 | body.chat #chat-preview #chat-preview-buttons > button { |
| 330 | padding: 0.5em; |
| 331 | flex: 0 1 auto; |
| 332 | margin: 0.25em 0; |
| 333 | } |
| 334 | |
| 335 | body.chat #chat-user-list-wrapper { |
| 336 | /* Safari can't do fieldsets right, so we emulate one. */ |
| 337 | border-radius: 0.5em; |
| 338 | margin: 1em 0 0.2em 0; |
| 339 | padding: 0 0.5em; |
| 340 | border-style: inset; |
| 341 | border-width: 0 1px 1px 1px/*else collides with the LEGEND*/; |
| 342 | } |
| 343 | body.chat #chat-user-list-wrapper.collapsed { |
| 344 | padding: 0; |
| 345 | } |
| 346 | body.chat #chat-user-list-wrapper > .legend { |
| 347 | font-weight: initial; |
| 348 | padding: 0 0.5em 0 0.5em; |
| 349 | position: relative; |
| 350 | top: -1.75ex/* place it like a fieldset legend */; |
| 351 | cursor: pointer; |
| 352 | } |
| 353 | body.chat #chat-user-list-wrapper > .legend > * { |
| 354 | vertical-align: middle; |
| 355 | } |
| 356 | body.chat #chat-user-list-wrapper > .legend > *:nth-child(2){ |
| 357 | /* Title label */ |
| 358 | opacity: 0.6; |
| 359 | font-size: 0.8em; |
| 360 | } |
| 361 | body.chat #chat-user-list-wrapper.collapsed > .legend > *:nth-child(2)::after { |
| 362 | content: " (tap to toggle)"; |
| 363 | } |
| 364 | body.chat #chat-user-list-wrapper .help-buttonlet { |
| 365 | margin: 0; |
| 366 | } |
| 367 | body.chat #chat-user-list-wrapper.collapsed #chat-user-list { |
| 368 | position: absolute !important; |
| 369 | opacity: 0 !important; |
| 370 | pointer-events: none !important; |
| 371 | display: none !important; |
| 372 | } |
| 373 | body.chat #chat-user-list { |
| 374 | margin-top: -1.25ex; |
| 375 | display: flex; |
| 376 | flex-direction: row; |
| 377 | flex-wrap: wrap; |
| 378 | align-items: center; |
| 379 | } |
| 380 | body.chat #chat-user-list .chat-user { |
| 381 | margin: 0.2em; |
| 382 | padding: 0.1em 0.5em 0.2em 0.5em; |
| 383 | border-radius: 0.5em; |
| 384 | cursor: pointer; |
| 385 | text-align: center; |
| 386 | white-space: pre; |
| 387 | } |
| 388 | body.chat #chat-user-list .timestamp { |
| 389 | font-size: 85%; |
| 390 | font-family: monospace; |
| 391 | } |
| 392 | body.chat #chat-user-list:not(.timestamps) .timestamp { |
| 393 | display: none; |
| 394 | } |
| 395 | body.chat #chat-user-list .chat-user.selected { |
| 396 | font-weight: bold; |
| 397 | text-decoration: underline; |
| 398 | } |
| 399 | |
| 400 | body.chat .anim-rotate-360 { |
| 401 | animation: rotate-360 750ms linear; |
| 402 | } |
| 403 | @keyframes rotate-360 { |
| 404 | from { transform: rotate(0deg); } |
| 405 | to { transform: rotate(360deg); } |
| 406 | } |
| 407 | body.chat .anim-flip-h { |
| 408 | animation: flip-h 750ms linear; |
| 409 | } |
| 410 | @keyframes flip-h{ |
| 411 | from { transform: rotateY(0deg); } |
| 412 | to { transform: rotateY(360deg); } |
| 413 | } |
| 414 | body.chat .anim-flip-v { |
| 415 | animation: flip-v 750ms linear; |
| 416 | } |
| 417 | @keyframes flip-v{ |
| 418 | from { transform: rotateX(0deg); } |
| 419 | to { transform: rotateX(360deg); } |
| 420 | } |
| 421 | body.chat .anim-fade-in { |
| 422 | animation: fade-in 750ms linear; |
| 423 | } |
| 424 | body.chat .anim-fade-in-fast { |
| 425 | animation: fade-in 350ms linear; |
| 426 | } |
| 427 | @keyframes fade-in { |
| 428 | from { opacity: 0; } |
| 429 | to { opacity: 1; } |
| 430 | } |
| 431 | body.chat .anim-fade-out-fast { |
| 432 | animation: fade-out 250ms linear; |
| 433 | } |
| 434 | @keyframes fade-out { |
| 435 | from { opacity: 1; } |
| 436 | to { opacity: 0; } |
| 437 | } |
| 438 |
+14
-5
| --- src/wikiformat.c | ||
| +++ src/wikiformat.c | ||
| @@ -1888,13 +1888,15 @@ | ||
| 1888 | 1888 | } |
| 1889 | 1889 | |
| 1890 | 1890 | /* |
| 1891 | 1891 | ** COMMAND: test-markdown-render |
| 1892 | 1892 | ** |
| 1893 | -** Usage: %fossil test-markdown-render FILE ... | |
| 1893 | +** Usage: %fossil test-markdown-render TEXT ... | |
| 1894 | 1894 | ** |
| 1895 | -** Render markdown in FILE as HTML on stdout. | |
| 1895 | +** Render markdown in TEXT as HTML on stdout, where TEXT | |
| 1896 | +** may be a file name or a markdown-formatted string. | |
| 1897 | +** | |
| 1896 | 1898 | ** Options: |
| 1897 | 1899 | ** |
| 1898 | 1900 | ** --safe Restrict the output to use only "safe" HTML |
| 1899 | 1901 | */ |
| 1900 | 1902 | void test_markdown_render(void){ |
| @@ -1904,13 +1906,20 @@ | ||
| 1904 | 1906 | db_find_and_open_repository(OPEN_OK_NOT_FOUND|OPEN_SUBSTITUTE,0); |
| 1905 | 1907 | bSafe = find_option("safe",0,0)!=0; |
| 1906 | 1908 | verify_all_options(); |
| 1907 | 1909 | for(i=2; i<g.argc; i++){ |
| 1908 | 1910 | blob_zero(&out); |
| 1909 | - blob_read_from_file(&in, g.argv[i], ExtFILE); | |
| 1910 | - if( g.argc>3 ){ | |
| 1911 | - fossil_print("<!------ %h ------->\n", g.argv[i]); | |
| 1911 | + if(file_isfile(g.argv[i], ExtFILE)){ | |
| 1912 | + blob_read_from_file(&in, g.argv[i], ExtFILE); | |
| 1913 | + if( g.argc>3 ){ | |
| 1914 | + fossil_print("<!------ %h ------->\n", g.argv[i]); | |
| 1915 | + } | |
| 1916 | + }else{ | |
| 1917 | + blob_init(&in, g.argv[i], -1); | |
| 1918 | + if( g.argc>3 ){ | |
| 1919 | + fossil_print("<!------ script #%d ------->\n", i-1); | |
| 1920 | + } | |
| 1912 | 1921 | } |
| 1913 | 1922 | markdown_to_html(&in, 0, &out); |
| 1914 | 1923 | safe_html_context( bSafe ? DOCSRC_UNTRUSTED : DOCSRC_TRUSTED ); |
| 1915 | 1924 | safe_html(&out); |
| 1916 | 1925 | blob_write_to_file(&out, "-"); |
| 1917 | 1926 |
| --- src/wikiformat.c | |
| +++ src/wikiformat.c | |
| @@ -1888,13 +1888,15 @@ | |
| 1888 | } |
| 1889 | |
| 1890 | /* |
| 1891 | ** COMMAND: test-markdown-render |
| 1892 | ** |
| 1893 | ** Usage: %fossil test-markdown-render FILE ... |
| 1894 | ** |
| 1895 | ** Render markdown in FILE as HTML on stdout. |
| 1896 | ** Options: |
| 1897 | ** |
| 1898 | ** --safe Restrict the output to use only "safe" HTML |
| 1899 | */ |
| 1900 | void test_markdown_render(void){ |
| @@ -1904,13 +1906,20 @@ | |
| 1904 | db_find_and_open_repository(OPEN_OK_NOT_FOUND|OPEN_SUBSTITUTE,0); |
| 1905 | bSafe = find_option("safe",0,0)!=0; |
| 1906 | verify_all_options(); |
| 1907 | for(i=2; i<g.argc; i++){ |
| 1908 | blob_zero(&out); |
| 1909 | blob_read_from_file(&in, g.argv[i], ExtFILE); |
| 1910 | if( g.argc>3 ){ |
| 1911 | fossil_print("<!------ %h ------->\n", g.argv[i]); |
| 1912 | } |
| 1913 | markdown_to_html(&in, 0, &out); |
| 1914 | safe_html_context( bSafe ? DOCSRC_UNTRUSTED : DOCSRC_TRUSTED ); |
| 1915 | safe_html(&out); |
| 1916 | blob_write_to_file(&out, "-"); |
| 1917 |
| --- src/wikiformat.c | |
| +++ src/wikiformat.c | |
| @@ -1888,13 +1888,15 @@ | |
| 1888 | } |
| 1889 | |
| 1890 | /* |
| 1891 | ** COMMAND: test-markdown-render |
| 1892 | ** |
| 1893 | ** Usage: %fossil test-markdown-render TEXT ... |
| 1894 | ** |
| 1895 | ** Render markdown in TEXT as HTML on stdout, where TEXT |
| 1896 | ** may be a file name or a markdown-formatted string. |
| 1897 | ** |
| 1898 | ** Options: |
| 1899 | ** |
| 1900 | ** --safe Restrict the output to use only "safe" HTML |
| 1901 | */ |
| 1902 | void test_markdown_render(void){ |
| @@ -1904,13 +1906,20 @@ | |
| 1906 | db_find_and_open_repository(OPEN_OK_NOT_FOUND|OPEN_SUBSTITUTE,0); |
| 1907 | bSafe = find_option("safe",0,0)!=0; |
| 1908 | verify_all_options(); |
| 1909 | for(i=2; i<g.argc; i++){ |
| 1910 | blob_zero(&out); |
| 1911 | if(file_isfile(g.argv[i], ExtFILE)){ |
| 1912 | blob_read_from_file(&in, g.argv[i], ExtFILE); |
| 1913 | if( g.argc>3 ){ |
| 1914 | fossil_print("<!------ %h ------->\n", g.argv[i]); |
| 1915 | } |
| 1916 | }else{ |
| 1917 | blob_init(&in, g.argv[i], -1); |
| 1918 | if( g.argc>3 ){ |
| 1919 | fossil_print("<!------ script #%d ------->\n", i-1); |
| 1920 | } |
| 1921 | } |
| 1922 | markdown_to_html(&in, 0, &out); |
| 1923 | safe_html_context( bSafe ? DOCSRC_UNTRUSTED : DOCSRC_TRUSTED ); |
| 1924 | safe_html(&out); |
| 1925 | blob_write_to_file(&out, "-"); |
| 1926 |
+14
-5
| --- src/wikiformat.c | ||
| +++ src/wikiformat.c | ||
| @@ -1888,13 +1888,15 @@ | ||
| 1888 | 1888 | } |
| 1889 | 1889 | |
| 1890 | 1890 | /* |
| 1891 | 1891 | ** COMMAND: test-markdown-render |
| 1892 | 1892 | ** |
| 1893 | -** Usage: %fossil test-markdown-render FILE ... | |
| 1893 | +** Usage: %fossil test-markdown-render TEXT ... | |
| 1894 | 1894 | ** |
| 1895 | -** Render markdown in FILE as HTML on stdout. | |
| 1895 | +** Render markdown in TEXT as HTML on stdout, where TEXT | |
| 1896 | +** may be a file name or a markdown-formatted string. | |
| 1897 | +** | |
| 1896 | 1898 | ** Options: |
| 1897 | 1899 | ** |
| 1898 | 1900 | ** --safe Restrict the output to use only "safe" HTML |
| 1899 | 1901 | */ |
| 1900 | 1902 | void test_markdown_render(void){ |
| @@ -1904,13 +1906,20 @@ | ||
| 1904 | 1906 | db_find_and_open_repository(OPEN_OK_NOT_FOUND|OPEN_SUBSTITUTE,0); |
| 1905 | 1907 | bSafe = find_option("safe",0,0)!=0; |
| 1906 | 1908 | verify_all_options(); |
| 1907 | 1909 | for(i=2; i<g.argc; i++){ |
| 1908 | 1910 | blob_zero(&out); |
| 1909 | - blob_read_from_file(&in, g.argv[i], ExtFILE); | |
| 1910 | - if( g.argc>3 ){ | |
| 1911 | - fossil_print("<!------ %h ------->\n", g.argv[i]); | |
| 1911 | + if(file_isfile(g.argv[i], ExtFILE)){ | |
| 1912 | + blob_read_from_file(&in, g.argv[i], ExtFILE); | |
| 1913 | + if( g.argc>3 ){ | |
| 1914 | + fossil_print("<!------ %h ------->\n", g.argv[i]); | |
| 1915 | + } | |
| 1916 | + }else{ | |
| 1917 | + blob_init(&in, g.argv[i], -1); | |
| 1918 | + if( g.argc>3 ){ | |
| 1919 | + fossil_print("<!------ script #%d ------->\n", i-1); | |
| 1920 | + } | |
| 1912 | 1921 | } |
| 1913 | 1922 | markdown_to_html(&in, 0, &out); |
| 1914 | 1923 | safe_html_context( bSafe ? DOCSRC_UNTRUSTED : DOCSRC_TRUSTED ); |
| 1915 | 1924 | safe_html(&out); |
| 1916 | 1925 | blob_write_to_file(&out, "-"); |
| 1917 | 1926 |
| --- src/wikiformat.c | |
| +++ src/wikiformat.c | |
| @@ -1888,13 +1888,15 @@ | |
| 1888 | } |
| 1889 | |
| 1890 | /* |
| 1891 | ** COMMAND: test-markdown-render |
| 1892 | ** |
| 1893 | ** Usage: %fossil test-markdown-render FILE ... |
| 1894 | ** |
| 1895 | ** Render markdown in FILE as HTML on stdout. |
| 1896 | ** Options: |
| 1897 | ** |
| 1898 | ** --safe Restrict the output to use only "safe" HTML |
| 1899 | */ |
| 1900 | void test_markdown_render(void){ |
| @@ -1904,13 +1906,20 @@ | |
| 1904 | db_find_and_open_repository(OPEN_OK_NOT_FOUND|OPEN_SUBSTITUTE,0); |
| 1905 | bSafe = find_option("safe",0,0)!=0; |
| 1906 | verify_all_options(); |
| 1907 | for(i=2; i<g.argc; i++){ |
| 1908 | blob_zero(&out); |
| 1909 | blob_read_from_file(&in, g.argv[i], ExtFILE); |
| 1910 | if( g.argc>3 ){ |
| 1911 | fossil_print("<!------ %h ------->\n", g.argv[i]); |
| 1912 | } |
| 1913 | markdown_to_html(&in, 0, &out); |
| 1914 | safe_html_context( bSafe ? DOCSRC_UNTRUSTED : DOCSRC_TRUSTED ); |
| 1915 | safe_html(&out); |
| 1916 | blob_write_to_file(&out, "-"); |
| 1917 |
| --- src/wikiformat.c | |
| +++ src/wikiformat.c | |
| @@ -1888,13 +1888,15 @@ | |
| 1888 | } |
| 1889 | |
| 1890 | /* |
| 1891 | ** COMMAND: test-markdown-render |
| 1892 | ** |
| 1893 | ** Usage: %fossil test-markdown-render TEXT ... |
| 1894 | ** |
| 1895 | ** Render markdown in TEXT as HTML on stdout, where TEXT |
| 1896 | ** may be a file name or a markdown-formatted string. |
| 1897 | ** |
| 1898 | ** Options: |
| 1899 | ** |
| 1900 | ** --safe Restrict the output to use only "safe" HTML |
| 1901 | */ |
| 1902 | void test_markdown_render(void){ |
| @@ -1904,13 +1906,20 @@ | |
| 1906 | db_find_and_open_repository(OPEN_OK_NOT_FOUND|OPEN_SUBSTITUTE,0); |
| 1907 | bSafe = find_option("safe",0,0)!=0; |
| 1908 | verify_all_options(); |
| 1909 | for(i=2; i<g.argc; i++){ |
| 1910 | blob_zero(&out); |
| 1911 | if(file_isfile(g.argv[i], ExtFILE)){ |
| 1912 | blob_read_from_file(&in, g.argv[i], ExtFILE); |
| 1913 | if( g.argc>3 ){ |
| 1914 | fossil_print("<!------ %h ------->\n", g.argv[i]); |
| 1915 | } |
| 1916 | }else{ |
| 1917 | blob_init(&in, g.argv[i], -1); |
| 1918 | if( g.argc>3 ){ |
| 1919 | fossil_print("<!------ script #%d ------->\n", i-1); |
| 1920 | } |
| 1921 | } |
| 1922 | markdown_to_html(&in, 0, &out); |
| 1923 | safe_html_context( bSafe ? DOCSRC_UNTRUSTED : DOCSRC_TRUSTED ); |
| 1924 | safe_html(&out); |
| 1925 | blob_write_to_file(&out, "-"); |
| 1926 |
+22
-26
| --- src/xfer.c | ||
| +++ src/xfer.c | ||
| @@ -1868,10 +1868,11 @@ | ||
| 1868 | 1868 | const char *zPCode = db_get("project-code", 0); |
| 1869 | 1869 | int nErr = 0; /* Number of errors */ |
| 1870 | 1870 | int nRoundtrip= 0; /* Number of HTTP requests */ |
| 1871 | 1871 | int nArtifactSent = 0; /* Total artifacts sent */ |
| 1872 | 1872 | int nArtifactRcvd = 0; /* Total artifacts received */ |
| 1873 | + int nPriorArtifact = 0; /* Artifacts received on prior round-trips */ | |
| 1873 | 1874 | const char *zOpType = 0;/* Push, Pull, Sync, Clone */ |
| 1874 | 1875 | double rSkew = 0.0; /* Maximum time skew */ |
| 1875 | 1876 | int uvHashSent = 0; /* The "pragma uv-hash" message has been sent */ |
| 1876 | 1877 | int uvDoPush = 0; /* Generate uvfile messages to send to server */ |
| 1877 | 1878 | int uvPullOnly = 0; /* 1: pull-only. 2: pull-only warning issued */ |
| @@ -2196,10 +2197,11 @@ | ||
| 2196 | 2197 | nCardSent++; |
| 2197 | 2198 | } |
| 2198 | 2199 | go = 0; |
| 2199 | 2200 | nUvGimmeSent = 0; |
| 2200 | 2201 | nUvFileRcvd = 0; |
| 2202 | + nPriorArtifact = nArtifactRcvd; | |
| 2201 | 2203 | |
| 2202 | 2204 | /* Process the reply that came back from the server */ |
| 2203 | 2205 | while( blob_line(&recv, &xfer.line) ){ |
| 2204 | 2206 | if( blob_buffer(&xfer.line)[0]=='#' ){ |
| 2205 | 2207 | const char *zLine = blob_buffer(&xfer.line); |
| @@ -2628,48 +2630,42 @@ | ||
| 2628 | 2630 | } |
| 2629 | 2631 | nUncRcvd += blob_size(&recv); |
| 2630 | 2632 | blob_reset(&recv); |
| 2631 | 2633 | nCycle++; |
| 2632 | 2634 | |
| 2633 | - /* If we received one or more files on the previous exchange but | |
| 2634 | - ** there are still phantoms, then go another round. | |
| 2635 | - */ | |
| 2635 | + /* Set go to 1 if we need to continue the sync/push/pull/clone for | |
| 2636 | + ** another round. Set go to 0 if it is time to quit. */ | |
| 2636 | 2637 | nFileRecv = xfer.nFileRcvd + xfer.nDeltaRcvd + xfer.nDanglingFile; |
| 2637 | 2638 | if( (nFileRecv>0 || newPhantom) && db_exists("SELECT 1 FROM phantom") ){ |
| 2638 | 2639 | go = 1; |
| 2639 | 2640 | mxPhantomReq = nFileRecv*2; |
| 2640 | 2641 | if( mxPhantomReq<200 ) mxPhantomReq = 200; |
| 2641 | 2642 | }else if( (syncFlags & SYNC_CLONE)!=0 && nFileRecv>0 ){ |
| 2642 | 2643 | go = 1; |
| 2644 | + }else if( xfer.nFileSent+xfer.nDeltaSent>0 || uvDoPush ){ | |
| 2645 | + /* Go another round if files are queued to send */ | |
| 2646 | + go = 1; | |
| 2647 | + }else if( xfer.nPrivIGot>0 && nCycle==1 ){ | |
| 2648 | + go = 1; | |
| 2649 | + }else if( (syncFlags & SYNC_CLONE)!=0 ){ | |
| 2650 | + if( nCycle==1 ){ | |
| 2651 | + go = 1; /* go at least two rounds on a clone */ | |
| 2652 | + }else if( cloneSeqno>0 && nArtifactRcvd>nPriorArtifact ){ | |
| 2653 | + /* Continue the clone until we see the clone_seqno 0" card or | |
| 2654 | + ** until we stop receiving artifacts */ | |
| 2655 | + go = 1; | |
| 2656 | + } | |
| 2657 | + }else if( nUvGimmeSent>0 && (nUvFileRcvd>0 || nCycle<3) ){ | |
| 2658 | + /* Continue looping as long as new uvfile cards are being received | |
| 2659 | + ** and uvgimme cards are being sent. */ | |
| 2660 | + go = 1; | |
| 2643 | 2661 | } |
| 2662 | + | |
| 2644 | 2663 | nCardRcvd = 0; |
| 2645 | 2664 | xfer.nFileRcvd = 0; |
| 2646 | 2665 | xfer.nDeltaRcvd = 0; |
| 2647 | 2666 | xfer.nDanglingFile = 0; |
| 2648 | - | |
| 2649 | - /* If we have one or more files queued to send, then go | |
| 2650 | - ** another round | |
| 2651 | - */ | |
| 2652 | - if( xfer.nFileSent+xfer.nDeltaSent>0 || uvDoPush ){ | |
| 2653 | - go = 1; | |
| 2654 | - } | |
| 2655 | - if( xfer.nPrivIGot>0 && nCycle==1 ) go = 1; | |
| 2656 | - | |
| 2657 | - /* If this is a clone, the go at least two rounds */ | |
| 2658 | - if( (syncFlags & SYNC_CLONE)!=0 && nCycle==1 ) go = 1; | |
| 2659 | - | |
| 2660 | - /* Stop the cycle if the server sends a "clone_seqno 0" card and | |
| 2661 | - ** we have gone at least two rounds. Always go at least two rounds | |
| 2662 | - ** on a clone in order to be sure to retrieve the configuration | |
| 2663 | - ** information which is only sent on the second round. | |
| 2664 | - */ | |
| 2665 | - if( cloneSeqno<=0 && nCycle>1 ) go = 0; | |
| 2666 | - | |
| 2667 | - /* Continue looping as long as new uvfile cards are being received | |
| 2668 | - ** and uvgimme cards are being sent. */ | |
| 2669 | - if( nUvGimmeSent>0 && (nUvFileRcvd>0 || nCycle<3) ) go = 1; | |
| 2670 | - | |
| 2671 | 2667 | db_multi_exec("DROP TABLE onremote; DROP TABLE unk;"); |
| 2672 | 2668 | if( go ){ |
| 2673 | 2669 | manifest_crosslink_end(MC_PERMIT_HOOKS); |
| 2674 | 2670 | }else{ |
| 2675 | 2671 | manifest_crosslink_end(MC_PERMIT_HOOKS); |
| 2676 | 2672 |
| --- src/xfer.c | |
| +++ src/xfer.c | |
| @@ -1868,10 +1868,11 @@ | |
| 1868 | const char *zPCode = db_get("project-code", 0); |
| 1869 | int nErr = 0; /* Number of errors */ |
| 1870 | int nRoundtrip= 0; /* Number of HTTP requests */ |
| 1871 | int nArtifactSent = 0; /* Total artifacts sent */ |
| 1872 | int nArtifactRcvd = 0; /* Total artifacts received */ |
| 1873 | const char *zOpType = 0;/* Push, Pull, Sync, Clone */ |
| 1874 | double rSkew = 0.0; /* Maximum time skew */ |
| 1875 | int uvHashSent = 0; /* The "pragma uv-hash" message has been sent */ |
| 1876 | int uvDoPush = 0; /* Generate uvfile messages to send to server */ |
| 1877 | int uvPullOnly = 0; /* 1: pull-only. 2: pull-only warning issued */ |
| @@ -2196,10 +2197,11 @@ | |
| 2196 | nCardSent++; |
| 2197 | } |
| 2198 | go = 0; |
| 2199 | nUvGimmeSent = 0; |
| 2200 | nUvFileRcvd = 0; |
| 2201 | |
| 2202 | /* Process the reply that came back from the server */ |
| 2203 | while( blob_line(&recv, &xfer.line) ){ |
| 2204 | if( blob_buffer(&xfer.line)[0]=='#' ){ |
| 2205 | const char *zLine = blob_buffer(&xfer.line); |
| @@ -2628,48 +2630,42 @@ | |
| 2628 | } |
| 2629 | nUncRcvd += blob_size(&recv); |
| 2630 | blob_reset(&recv); |
| 2631 | nCycle++; |
| 2632 | |
| 2633 | /* If we received one or more files on the previous exchange but |
| 2634 | ** there are still phantoms, then go another round. |
| 2635 | */ |
| 2636 | nFileRecv = xfer.nFileRcvd + xfer.nDeltaRcvd + xfer.nDanglingFile; |
| 2637 | if( (nFileRecv>0 || newPhantom) && db_exists("SELECT 1 FROM phantom") ){ |
| 2638 | go = 1; |
| 2639 | mxPhantomReq = nFileRecv*2; |
| 2640 | if( mxPhantomReq<200 ) mxPhantomReq = 200; |
| 2641 | }else if( (syncFlags & SYNC_CLONE)!=0 && nFileRecv>0 ){ |
| 2642 | go = 1; |
| 2643 | } |
| 2644 | nCardRcvd = 0; |
| 2645 | xfer.nFileRcvd = 0; |
| 2646 | xfer.nDeltaRcvd = 0; |
| 2647 | xfer.nDanglingFile = 0; |
| 2648 | |
| 2649 | /* If we have one or more files queued to send, then go |
| 2650 | ** another round |
| 2651 | */ |
| 2652 | if( xfer.nFileSent+xfer.nDeltaSent>0 || uvDoPush ){ |
| 2653 | go = 1; |
| 2654 | } |
| 2655 | if( xfer.nPrivIGot>0 && nCycle==1 ) go = 1; |
| 2656 | |
| 2657 | /* If this is a clone, the go at least two rounds */ |
| 2658 | if( (syncFlags & SYNC_CLONE)!=0 && nCycle==1 ) go = 1; |
| 2659 | |
| 2660 | /* Stop the cycle if the server sends a "clone_seqno 0" card and |
| 2661 | ** we have gone at least two rounds. Always go at least two rounds |
| 2662 | ** on a clone in order to be sure to retrieve the configuration |
| 2663 | ** information which is only sent on the second round. |
| 2664 | */ |
| 2665 | if( cloneSeqno<=0 && nCycle>1 ) go = 0; |
| 2666 | |
| 2667 | /* Continue looping as long as new uvfile cards are being received |
| 2668 | ** and uvgimme cards are being sent. */ |
| 2669 | if( nUvGimmeSent>0 && (nUvFileRcvd>0 || nCycle<3) ) go = 1; |
| 2670 | |
| 2671 | db_multi_exec("DROP TABLE onremote; DROP TABLE unk;"); |
| 2672 | if( go ){ |
| 2673 | manifest_crosslink_end(MC_PERMIT_HOOKS); |
| 2674 | }else{ |
| 2675 | manifest_crosslink_end(MC_PERMIT_HOOKS); |
| 2676 |
| --- src/xfer.c | |
| +++ src/xfer.c | |
| @@ -1868,10 +1868,11 @@ | |
| 1868 | const char *zPCode = db_get("project-code", 0); |
| 1869 | int nErr = 0; /* Number of errors */ |
| 1870 | int nRoundtrip= 0; /* Number of HTTP requests */ |
| 1871 | int nArtifactSent = 0; /* Total artifacts sent */ |
| 1872 | int nArtifactRcvd = 0; /* Total artifacts received */ |
| 1873 | int nPriorArtifact = 0; /* Artifacts received on prior round-trips */ |
| 1874 | const char *zOpType = 0;/* Push, Pull, Sync, Clone */ |
| 1875 | double rSkew = 0.0; /* Maximum time skew */ |
| 1876 | int uvHashSent = 0; /* The "pragma uv-hash" message has been sent */ |
| 1877 | int uvDoPush = 0; /* Generate uvfile messages to send to server */ |
| 1878 | int uvPullOnly = 0; /* 1: pull-only. 2: pull-only warning issued */ |
| @@ -2196,10 +2197,11 @@ | |
| 2197 | nCardSent++; |
| 2198 | } |
| 2199 | go = 0; |
| 2200 | nUvGimmeSent = 0; |
| 2201 | nUvFileRcvd = 0; |
| 2202 | nPriorArtifact = nArtifactRcvd; |
| 2203 | |
| 2204 | /* Process the reply that came back from the server */ |
| 2205 | while( blob_line(&recv, &xfer.line) ){ |
| 2206 | if( blob_buffer(&xfer.line)[0]=='#' ){ |
| 2207 | const char *zLine = blob_buffer(&xfer.line); |
| @@ -2628,48 +2630,42 @@ | |
| 2630 | } |
| 2631 | nUncRcvd += blob_size(&recv); |
| 2632 | blob_reset(&recv); |
| 2633 | nCycle++; |
| 2634 | |
| 2635 | /* Set go to 1 if we need to continue the sync/push/pull/clone for |
| 2636 | ** another round. Set go to 0 if it is time to quit. */ |
| 2637 | nFileRecv = xfer.nFileRcvd + xfer.nDeltaRcvd + xfer.nDanglingFile; |
| 2638 | if( (nFileRecv>0 || newPhantom) && db_exists("SELECT 1 FROM phantom") ){ |
| 2639 | go = 1; |
| 2640 | mxPhantomReq = nFileRecv*2; |
| 2641 | if( mxPhantomReq<200 ) mxPhantomReq = 200; |
| 2642 | }else if( (syncFlags & SYNC_CLONE)!=0 && nFileRecv>0 ){ |
| 2643 | go = 1; |
| 2644 | }else if( xfer.nFileSent+xfer.nDeltaSent>0 || uvDoPush ){ |
| 2645 | /* Go another round if files are queued to send */ |
| 2646 | go = 1; |
| 2647 | }else if( xfer.nPrivIGot>0 && nCycle==1 ){ |
| 2648 | go = 1; |
| 2649 | }else if( (syncFlags & SYNC_CLONE)!=0 ){ |
| 2650 | if( nCycle==1 ){ |
| 2651 | go = 1; /* go at least two rounds on a clone */ |
| 2652 | }else if( cloneSeqno>0 && nArtifactRcvd>nPriorArtifact ){ |
| 2653 | /* Continue the clone until we see the clone_seqno 0" card or |
| 2654 | ** until we stop receiving artifacts */ |
| 2655 | go = 1; |
| 2656 | } |
| 2657 | }else if( nUvGimmeSent>0 && (nUvFileRcvd>0 || nCycle<3) ){ |
| 2658 | /* Continue looping as long as new uvfile cards are being received |
| 2659 | ** and uvgimme cards are being sent. */ |
| 2660 | go = 1; |
| 2661 | } |
| 2662 | |
| 2663 | nCardRcvd = 0; |
| 2664 | xfer.nFileRcvd = 0; |
| 2665 | xfer.nDeltaRcvd = 0; |
| 2666 | xfer.nDanglingFile = 0; |
| 2667 | db_multi_exec("DROP TABLE onremote; DROP TABLE unk;"); |
| 2668 | if( go ){ |
| 2669 | manifest_crosslink_end(MC_PERMIT_HOOKS); |
| 2670 | }else{ |
| 2671 | manifest_crosslink_end(MC_PERMIT_HOOKS); |
| 2672 |
+25
| --- www/chat.md | ||
| +++ www/chat.md | ||
| @@ -96,10 +96,35 @@ | ||
| 96 | 96 | By default, the list of new-message notification sounds is limited to |
| 97 | 97 | a few built in to the fossil binary. In addition, any |
| 98 | 98 | [unversioned files](./unvers.wiki) named `alert-sounds/*.{mp3,wav,ogg}` |
| 99 | 99 | will be included in that list. To switch sounds, tap the "settings" |
| 100 | 100 | button. |
| 101 | + | |
| 102 | +### <a id='connection'></a> Who's Online? | |
| 103 | + | |
| 104 | +Because the chat app has to be able to work over transient CGI-based | |
| 105 | +connections, as opposed to a stable socket connection to the server, | |
| 106 | +real-time tracking of "who's online" is not feasible. As of version | |
| 107 | +2.17, chat offers an optional feature, toggleable in the settings, | |
| 108 | +which can list users who have posted messages in the client's current | |
| 109 | +list of loaded messages. This is not the same thing as tracking who's | |
| 110 | +online, but it gives an overview of which users have been active most | |
| 111 | +recently, noting that "lurkers" (people who post no messages) will not | |
| 112 | +show up in that list, nor does the chat infrastructure have a way to | |
| 113 | +track and present those. That list can be used to filter messages on a | |
| 114 | +specific user by tapping on that user's name, tapping a second time to | |
| 115 | +remove the filter. | |
| 116 | + | |
| 117 | +Sidebar: message deletion is a type of message and deletions count | |
| 118 | +towards updates in the recent activity list (counted for the person | |
| 119 | +who performed the deletion, not the author of the deleted | |
| 120 | +comment). That can potentially lead to odd corner cases where a user | |
| 121 | +shows up in the list but has no messages which are currently visible | |
| 122 | +because they were deleted, or an admin user who has not posted | |
| 123 | +anything but deleted a message. That is a known minor cosmetic-only | |
| 124 | +bug with a resolution of "will not fix." | |
| 125 | + | |
| 101 | 126 | |
| 102 | 127 | ## Implementation Details |
| 103 | 128 | |
| 104 | 129 | *You do not need to understand how Fossil chat works in order to use it. |
| 105 | 130 | But many developers prefer to know how their tools work. |
| 106 | 131 |
| --- www/chat.md | |
| +++ www/chat.md | |
| @@ -96,10 +96,35 @@ | |
| 96 | By default, the list of new-message notification sounds is limited to |
| 97 | a few built in to the fossil binary. In addition, any |
| 98 | [unversioned files](./unvers.wiki) named `alert-sounds/*.{mp3,wav,ogg}` |
| 99 | will be included in that list. To switch sounds, tap the "settings" |
| 100 | button. |
| 101 | |
| 102 | ## Implementation Details |
| 103 | |
| 104 | *You do not need to understand how Fossil chat works in order to use it. |
| 105 | But many developers prefer to know how their tools work. |
| 106 |
| --- www/chat.md | |
| +++ www/chat.md | |
| @@ -96,10 +96,35 @@ | |
| 96 | By default, the list of new-message notification sounds is limited to |
| 97 | a few built in to the fossil binary. In addition, any |
| 98 | [unversioned files](./unvers.wiki) named `alert-sounds/*.{mp3,wav,ogg}` |
| 99 | will be included in that list. To switch sounds, tap the "settings" |
| 100 | button. |
| 101 | |
| 102 | ### <a id='connection'></a> Who's Online? |
| 103 | |
| 104 | Because the chat app has to be able to work over transient CGI-based |
| 105 | connections, as opposed to a stable socket connection to the server, |
| 106 | real-time tracking of "who's online" is not feasible. As of version |
| 107 | 2.17, chat offers an optional feature, toggleable in the settings, |
| 108 | which can list users who have posted messages in the client's current |
| 109 | list of loaded messages. This is not the same thing as tracking who's |
| 110 | online, but it gives an overview of which users have been active most |
| 111 | recently, noting that "lurkers" (people who post no messages) will not |
| 112 | show up in that list, nor does the chat infrastructure have a way to |
| 113 | track and present those. That list can be used to filter messages on a |
| 114 | specific user by tapping on that user's name, tapping a second time to |
| 115 | remove the filter. |
| 116 | |
| 117 | Sidebar: message deletion is a type of message and deletions count |
| 118 | towards updates in the recent activity list (counted for the person |
| 119 | who performed the deletion, not the author of the deleted |
| 120 | comment). That can potentially lead to odd corner cases where a user |
| 121 | shows up in the list but has no messages which are currently visible |
| 122 | because they were deleted, or an admin user who has not posted |
| 123 | anything but deleted a message. That is a known minor cosmetic-only |
| 124 | bug with a resolution of "will not fix." |
| 125 | |
| 126 | |
| 127 | ## Implementation Details |
| 128 | |
| 129 | *You do not need to understand how Fossil chat works in order to use it. |
| 130 | But many developers prefer to know how their tools work. |
| 131 |