@@ -134,7 +134,7 @@ function bindQuestionControls(scope) {
134134 button . textContent = 'Проверяем…' ;
135135 try {
136136 const result = await api ( '/api/code/check' , { method : 'POST' , body : JSON . stringify ( { question_id : questionId , answer : editor . value } ) } ) ;
137- showInline ( questionId , result . correct , result . message , result . checks ) ;
137+ showInline ( questionId , result . correct , result . message , result . checks , result . error_hint ) ;
138138 } catch ( error ) { showInline ( questionId , false , error . message ) ; }
139139 button . disabled = false ;
140140 button . textContent = '▷ Проверить код' ;
@@ -167,12 +167,15 @@ function showCodeOutput(questionId, result) {
167167 node . textContent = output . join ( '\n' ) || 'Нет вывода.' ;
168168}
169169
170- function showInline ( questionId , correct , message , checks = [ ] ) {
170+ function showInline ( questionId , correct , message , checks = [ ] , errorHint = null ) {
171171 const node = document . querySelector ( `#result-${ questionId } ` ) ;
172172 if ( ! node ) return ;
173173 const details = checks . length && ! correct ? ` <small>(${ checks . filter ( ( item ) => ! item . passed ) . map ( ( item ) => `ожидалось ${ esc ( item . expected ) } , получено ${ esc ( item . actual ) } ` ) . join ( '; ' ) } )</small>` : '' ;
174174 node . className = `inline-result visible ${ correct ? 'ok' : 'no' } ` ;
175- node . innerHTML = `${ correct ? '✓' : '↺' } ${ esc ( message ) } ${ details } ` ;
175+ const hint = errorHint
176+ ? `<div class="error-hint"><strong>${ esc ( errorHint . title ) } </strong><span>${ esc ( errorHint . hint ) } </span></div><small class="raw-error">${ esc ( errorHint . original ) } </small>`
177+ : `${ correct ? '✓' : '↺' } ${ esc ( message ) } ` ;
178+ node . innerHTML = `${ hint } ${ details } ` ;
176179}
177180
178181function submissionResult ( result , retryText = 'Попробовать ещё раз' ) {
@@ -203,7 +206,7 @@ async function renderLesson(id) {
203206 const result = await api ( `/api/lessons/${ id } /submit` , { method : 'POST' , body : JSON . stringify ( { answers : getAnswers ( form , lesson . questions ) } ) } ) ;
204207 document . querySelector ( '#submission-result' ) ?. remove ( ) ;
205208 form . insertAdjacentHTML ( 'afterend' , submissionResult ( result ) ) ;
206- result . results . forEach ( ( item ) => showInline ( item . question_id , item . correct , item . message , item . checks ) ) ;
209+ result . results . forEach ( ( item ) => showInline ( item . question_id , item . correct , item . message , item . checks , item . error_hint ) ) ;
207210 await refreshDashboard ( ) ;
208211 if ( result . passed ) toast ( `+${ result . xp_gained } XP — урок пройден!` ) ;
209212 } catch ( error ) { toast ( error . message ) ; }
@@ -223,7 +226,7 @@ async function renderPractice() {
223226 const answer = getAnswers ( form , [ question ] ) [ 0 ] ;
224227 try {
225228 const result = await api ( '/api/practice/submit' , { method : 'POST' , body : JSON . stringify ( answer ) } ) ;
226- showInline ( question . id , result . correct , result . message , result . checks ) ;
229+ showInline ( question . id , result . correct , result . message , result . checks , result . error_hint ) ;
227230 await refreshDashboard ( ) ;
228231 if ( result . correct ) toast ( `Верно! +${ result . xp_gained } XP` ) ;
229232 } catch ( error ) { toast ( error . message ) ; }
@@ -243,7 +246,7 @@ async function renderExam(moduleId) {
243246 const result = await api ( `/api/exams/${ moduleId } /submit` , { method : 'POST' , body : JSON . stringify ( { answers : getAnswers ( form , exam . questions ) } ) } ) ;
244247 document . querySelector ( '#submission-result' ) ?. remove ( ) ;
245248 form . insertAdjacentHTML ( 'afterend' , submissionResult ( result , 'Пересдать экзамен' ) ) ;
246- result . results . forEach ( ( item ) => showInline ( item . question_id , item . correct , item . message , item . checks ) ) ;
249+ result . results . forEach ( ( item ) => showInline ( item . question_id , item . correct , item . message , item . checks , item . error_hint ) ) ;
247250 await refreshDashboard ( ) ;
248251 if ( result . passed ) toast ( `Экзамен сдан! +${ result . xp_gained } XP` ) ;
249252 } catch ( error ) { toast ( error . message ) ; }
@@ -272,5 +275,41 @@ document.querySelector('#reset-button').addEventListener('click', async () => {
272275 location . hash = '#/' ;
273276 render ( ) ;
274277} ) ;
278+
279+ document . addEventListener ( 'keydown' , ( event ) => {
280+ const editor = event . target . closest ( '.code-editor' ) ;
281+ if ( ! editor ) return ;
282+ const { value, selectionStart : start , selectionEnd : end } = editor ;
283+ const lineStart = value . lastIndexOf ( '\n' , start - 1 ) + 1 ;
284+ const lineEndIndex = value . indexOf ( '\n' , Math . max ( start , end - 1 ) ) ;
285+ const lineEnd = lineEndIndex < 0 ? value . length : lineEndIndex ;
286+
287+ if ( event . key === 'Enter' ) {
288+ const line = value . slice ( lineStart , lineEnd ) ;
289+ const indent = ( line . match ( / ^ \s * / ) || [ '' ] ) [ 0 ] + ( line . trimEnd ( ) . endsWith ( ':' ) ? ' ' : '' ) ;
290+ event . preventDefault ( ) ;
291+ editor . setRangeText ( `\n${ indent } ` , start , end , 'end' ) ;
292+ return ;
293+ }
294+
295+ if ( event . key !== 'Tab' ) return ;
296+ event . preventDefault ( ) ;
297+ const selected = value . slice ( lineStart , lineEnd ) ;
298+ if ( ! event . shiftKey && ! selected . includes ( '\n' ) ) {
299+ editor . setRangeText ( ' ' , start , end , 'end' ) ;
300+ return ;
301+ }
302+ if ( event . shiftKey ) {
303+ const unindented = selected . replace ( / ^ { 1 , 4 } / gm, '' ) ;
304+ const removedBeforeCursor = Math . min ( 4 , ( value . slice ( lineStart , start ) . match ( / ^ * / ) || [ '' ] ) [ 0 ] . length ) ;
305+ editor . value = value . slice ( 0 , lineStart ) + unindented + value . slice ( lineEnd ) ;
306+ editor . setSelectionRange ( start - removedBeforeCursor , end - ( selected . length - unindented . length ) ) ;
307+ } else {
308+ const indented = selected . replace ( / ^ / gm, ' ' ) ;
309+ const lines = selected . split ( '\n' ) . length ;
310+ editor . value = value . slice ( 0 , lineStart ) + indented + value . slice ( lineEnd ) ;
311+ editor . setSelectionRange ( start + 4 , end + lines * 4 ) ;
312+ }
313+ } ) ;
275314window . addEventListener ( 'hashchange' , render ) ;
276315render ( ) ;
0 commit comments